Linter and requirements updates (#575)

* Use black, flake8, and isort instead of just flake8
* Reorganize requirements for simplicity
* Update some rules
* Add a script for automatically fixing linter errors
* Add some docs on linting
* Run travis linter on python 3.6
This commit is contained in:
William Lachance 2020-03-27 16:17:37 -04:00 коммит произвёл GitHub
Родитель db35d9ef03
Коммит 40890ec5b2
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
95 изменённых файлов: 3651 добавлений и 3340 удалений

1
.linter-files Normal file
Просмотреть файл

@ -0,0 +1 @@
mozregression tests setup.py gui/mozregui gui/build.py gui/tests

Просмотреть файл

@ -7,14 +7,23 @@ matrix:
dist: trusty # newer dists seem to have some weird ssl error with python2
language: python
python: '2.7'
virtualenv:
system_site_packages: true
script:
- pip install -e .
- coverage run -m pytest tests && mv .coverage .coverage.core
- coverage combine
- pip install coveralls; coveralls
- name: python35-linux
env: PYTHON=python3.5
os: linux
language: python
python: '3.5'
script:
- pip install -e .
- coverage run -m pytest tests && mv .coverage .coverage.core
- coverage combine
- pip install coveralls; coveralls
- name: python3-linux
env: PYTHON=python3.5
env: PYTHON=python3.6
os: linux
addons:
apt:
@ -24,22 +33,22 @@ matrix:
services:
- xvfb
language: python
python: '3.5'
python: '3.6'
script:
- pip install -r requirements-gui-dev.txt
- pip install -r requirements/all.txt
- coverage run -m pytest tests && mv .coverage .coverage.core
- coverage run gui/build.py test && mv .coverage .coverage.gui
- coverage combine
- pip install coveralls; coveralls
- python gui/build.py bundle
- name: linters
env: PYTHON=python3.5
env: PYTHON=python3.7
os: linux
language: python
python: '3.5'
python: '3.7'
script:
- flake8 mozregression tests setup.py
- flake8 gui/mozregui gui/build.py gui/tests
- pip install -r requirements/linters.txt
- ./bin/lint-check.sh || (echo "Lint fix results:" && ./bin/lint-fix.sh && git diff && false)
install:
- if [ "$TRAVIS_OS_NAME" == "osx" ]; then
@ -56,7 +65,7 @@ install:
sudo install_name_tool -id $PWD/QtGui.so QtGui.so;
cd $MOZPATH;
fi
- pip install -r requirements-dev.txt
- pip install -r requirements/console.txt
deploy:
- provider: releases

Просмотреть файл

@ -3,20 +3,21 @@
mozregression is an interactive regression rangefinder for quickly tracking down the source of bugs in Mozilla nightly and inbound builds.
You can start using mozregression today:
- [start with our installation guide](https://mozilla.github.io/mozregression/install.html), then
- take a look at [our Quick Start document](https://mozilla.github.io/mozregression/quickstart.html).
- [start with our installation guide](https://mozilla.github.io/mozregression/install.html), then
- take a look at [our Quick Start document](https://mozilla.github.io/mozregression/quickstart.html).
## Status
[![Latest Version](https://img.shields.io/pypi/v/mozregression.svg)](https://pypi.python.org/pypi/mozregression/)
[![License](https://img.shields.io/pypi/l/mozregression.svg)](https://pypi.python.org/pypi/mozregression/)
Build status:
- Linux:
[![Linux Build Status](https://travis-ci.org/mozilla/mozregression.svg?branch=master)](https://travis-ci.org/mozilla/mozregression)
[![Coverage Status](https://img.shields.io/coveralls/mozilla/mozregression.svg)](https://coveralls.io/r/mozilla/mozregression)
- Windows: [![Windows Build status](https://ci.appveyor.com/api/projects/status/bcg7t1pt2bahggdr?svg=true)](https://ci.appveyor.com/project/wlach/mozregression/branch/master)
- Linux:
[![Linux Build Status](https://travis-ci.org/mozilla/mozregression.svg?branch=master)](https://travis-ci.org/mozilla/mozregression)
[![Coverage Status](https://img.shields.io/coveralls/mozilla/mozregression.svg)](https://coveralls.io/r/mozilla/mozregression)
- Windows: [![Windows Build status](https://ci.appveyor.com/api/projects/status/bcg7t1pt2bahggdr?svg=true)](https://ci.appveyor.com/project/wlach/mozregression/branch/master)
For more information see:
@ -41,27 +42,39 @@ This is recommended.
If you are **really sure** that you only want to hack on the mozregression command line:
1. Install [virtualenv](https://virtualenv.pypa.io/en/stable/)
or [virtualenvwrapper](https://virtualenvwrapper.readthedocs.org/en/latest/).
or [virtualenvwrapper](https://virtualenvwrapper.readthedocs.org/en/latest/).
2. install dependencies:
With virtualenvwrapper:
With virtualenvwrapper:
```bash
mkvirtualenv -p /usr/bin/python3 mozregression
pip install -r requirements-dev.txt
```
```bash
mkvirtualenv -p /usr/bin/python3 mozregression
pip install -r requirements/all-console.txt
```
Or with virtualenv: ::
Or with virtualenv: ::
```bash
virtualenv -p /usr/bin/python3 venv
source venv/bin/activate
pip install -r requirements-dev.txt
```
```bash
virtualenv -p /usr/bin/python3 venv
source venv/bin/activate
pip install -r requirements/all-console.txt
```
2. lint your code for errors and formatting (we use [black](https://black.readthedocs.io/en/stable/), [flake8](https://flake8.pycqa.org/en/latest/) and [isort](https://isort.readthedocs.io/en/latest/))
```bash
./bin/lint-check.sh
```
If it turns up errors, try using the `lint-fix.sh` script to fix any errors which can be addressed automatically:
```bash
./bin/lint-fix.sh
```
3. run tests (be sure that your virtualenv is activated):
```bash
pytest tests
```
```bash
pytest tests
```

Просмотреть файл

@ -8,7 +8,7 @@ init:
- "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%"
install:
# install mozregression code and test dependencies
- "pip install -r requirements-gui-dev.txt"
- "pip install -r requirements/all.txt"
test_script:
- "python setup.py test"
- "python gui\\build.py test"

8
bin/lint-check.sh Executable file
Просмотреть файл

@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -e
LINTER_FILES="$(dirname "$0")/../.linter-files"
cat $LINTER_FILES | xargs isort --check-only --recursive
cat $LINTER_FILES | xargs flake8

8
bin/lint-fix.sh Executable file
Просмотреть файл

@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -e
LINTER_FILES="$(dirname "$0")/../.linter-files"
cat $LINTER_FILES | xargs isort --recursive -y
cat $LINTER_FILES | xargs black

Просмотреть файл

@ -23,18 +23,18 @@ a virtualenv here. See this link
about python virtualenvs. You may also consider using virtualenvwrapper
(https://virtualenvwrapper.readthedocs.org/en/latest/).
Python 3.5+ is *required* to develop or use mozregression-gui.
Python 3.6+ is *required* to develop or use mozregression-gui.
Install with virtualenvwrapper: ::
mkvirtualenv -p /usr/bin/python3 mozregression
pip install -r requirements-gui-dev.txt
pip install -r requirements/all.txt
Or with virtualenv: ::
virtualenv --system-site-packages -p /usr/bin/python3 venv
source venv/bin/activate
pip install -r requirements-gui-dev.txt
pip install -r requirements/all.txt
Launching the application
-------------------------

Просмотреть файл

@ -5,63 +5,68 @@ See python build.py --help
"""
import argparse
import sys
import subprocess
import os
import shutil
import glob
import os
import pipes
import shutil
import subprocess
import sys
import tarfile
IS_WIN = os.name == 'nt'
IS_MAC = sys.platform == 'darwin'
IS_WIN = os.name == "nt"
IS_MAC = sys.platform == "darwin"
def call(*args, **kwargs):
print('Executing `%s`' % ' '.join(pipes.quote(a) for a in args))
print("Executing `%s`" % " ".join(pipes.quote(a) for a in args))
subprocess.check_call(args, **kwargs)
def py_script(script_name):
python_dir = os.path.dirname(sys.executable)
if IS_WIN:
return os.path.join(python_dir, 'Scripts',
script_name + '.exe')
return os.path.join(python_dir, "Scripts", script_name + ".exe")
else:
return os.path.join(python_dir, script_name)
def do_uic(options, force=False):
for uifile in glob.glob('mozregui/ui/*.ui'):
pyfile = os.path.splitext(uifile)[0] + '.py'
if force or not os.path.isfile(pyfile) or \
(os.path.getmtime(uifile) > os.path.getmtime(pyfile)):
for uifile in glob.glob("mozregui/ui/*.ui"):
pyfile = os.path.splitext(uifile)[0] + ".py"
if (
force
or not os.path.isfile(pyfile)
or (os.path.getmtime(uifile) > os.path.getmtime(pyfile))
):
print("uic'ing %s -> %s" % (uifile, pyfile))
os.system('pyside2-uic {} > {}'.format(uifile, pyfile))
os.system("pyside2-uic {} > {}".format(uifile, pyfile))
def do_rcc(options, force=False):
rccfile = 'resources.qrc'
pyfile = 'resources_rc.py'
if force or not os.path.isfile(pyfile) or \
(os.path.getmtime(rccfile) > os.path.getmtime(pyfile)):
rccfile = "resources.qrc"
pyfile = "resources_rc.py"
if (
force
or not os.path.isfile(pyfile)
or (os.path.getmtime(rccfile) > os.path.getmtime(pyfile))
):
print("rcc'ing %s -> %s" % (rccfile, pyfile))
call('pyside2-rcc', '-o', pyfile, rccfile)
call("pyside2-rcc", "-o", pyfile, rccfile)
def do_run(options):
do_uic(options)
do_rcc(options)
call(sys.executable, 'mozregression-gui.py')
call(sys.executable, "mozregression-gui.py")
def do_test(options):
do_uic(options)
do_rcc(options)
print('Running tests...')
print("Running tests...")
import pytest
sys.exit(pytest.main(['tests', '-v']))
sys.exit(pytest.main(["tests", "-v"]))
def do_bundle(options):
@ -69,45 +74,47 @@ def do_bundle(options):
do_rcc(options, True)
# clean previous runs
for dirname in ('build', 'dist'):
for dirname in ("build", "dist"):
if os.path.isdir(dirname):
shutil.rmtree(dirname)
# create a bundle for the application
call('pyinstaller', 'gui.spec')
call("pyinstaller", "gui.spec")
# create an installer
if IS_WIN:
makensis_path = os.path.join(options.nsis_path, "makensis.exe")
call(makensis_path, 'wininst.nsi', cwd='wininst')
call(makensis_path, "wininst.nsi", cwd="wininst")
elif IS_MAC:
call('hdiutil', 'create', 'dist/mozregression-gui.dmg',
'-srcfolder', 'dist/', '-ov')
call(
"hdiutil", "create", "dist/mozregression-gui.dmg", "-srcfolder", "dist/", "-ov",
)
else:
with tarfile.open('mozregression-gui.tar.gz', 'w:gz') as tar:
tar.add(r'dist/')
with tarfile.open("mozregression-gui.tar.gz", "w:gz") as tar:
tar.add(r"dist/")
def parse_args():
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
uic = subparsers.add_parser('uic', help='build uic files')
uic = subparsers.add_parser("uic", help="build uic files")
uic.set_defaults(func=do_uic)
rcc = subparsers.add_parser('rcc', help='build rcc files')
rcc = subparsers.add_parser("rcc", help="build rcc files")
rcc.set_defaults(func=do_rcc)
run = subparsers.add_parser('run', help='run the application')
run = subparsers.add_parser("run", help="run the application")
run.set_defaults(func=do_run)
test = subparsers.add_parser('test', help='run the unit tests')
test = subparsers.add_parser("test", help="run the unit tests")
test.set_defaults(func=do_test)
bundle = subparsers.add_parser('bundle',
help='bundle the application (freeze)')
bundle = subparsers.add_parser("bundle", help="bundle the application (freeze)")
if IS_WIN:
bundle.add_argument('--nsis-path', default='C:\\NSIS',
help='your NSIS path on the'
' system(default: %(default)r)')
bundle.add_argument(
"--nsis-path",
default="C:\\NSIS",
help="your NSIS path on the" " system(default: %(default)r)",
)
bundle.set_defaults(func=do_bundle)
@ -122,8 +129,8 @@ def main():
try:
options.func(options)
except Exception as e:
sys.exit('ERROR: %s' % e)
sys.exit("ERROR: %s" % e)
if __name__ == '__main__':
if __name__ == "__main__":
main()

Просмотреть файл

@ -1,6 +1,6 @@
from PySide2.QtCore import QAbstractListModel, QModelIndex, Qt, \
Slot
from PySide2.QtWidgets import QWidget, QFileDialog
from PySide2.QtCore import QAbstractListModel, QModelIndex, Qt, Slot
from PySide2.QtWidgets import QFileDialog, QWidget
from mozregui.ui.addons_editor import Ui_AddonsEditor
@ -26,8 +26,7 @@ class AddonsModel(QAbstractListModel):
def add_addon(self, addon):
if addon:
addons_list_length = len(self.addons)
self.beginInsertRows(QModelIndex(), addons_list_length,
addons_list_length)
self.beginInsertRows(QModelIndex(), addons_list_length, addons_list_length)
self.addons.append(addon)
self.endInsertRows()
@ -41,6 +40,7 @@ class AddonsWidgetEditor(QWidget):
"""
A widget to add or remove addons, and buttons to let the user interact.
"""
def __init__(self, parent=None):
QWidget.__init__(self, parent)
self.ui = Ui_AddonsEditor()
@ -52,18 +52,15 @@ class AddonsWidgetEditor(QWidget):
@Slot()
def add_addon(self):
(fileNames, _) = QFileDialog.getOpenFileNames(
self,
"Choose one or more addon files",
filter="addon file (*.xpi)",
self, "Choose one or more addon files", filter="addon file (*.xpi)",
)
for fileName in fileNames:
self.list_model.add_addon(fileName)
self.list_model.add_addon(fileName)
@Slot()
def remove_selected_addons(self):
selected_rows = sorted(
set(i.row() for i in self.ui.list_view.selectedIndexes()),
reverse=True
set(i.row() for i in self.ui.list_view.selectedIndexes()), reverse=True
)
for row in selected_rows:
self.list_model.remove_pref(row)
@ -72,7 +69,7 @@ class AddonsWidgetEditor(QWidget):
return self.list_model.addons
if __name__ == '__main__':
if __name__ == "__main__":
from PySide2.QtWidgets import QApplication
app = QApplication([])

Просмотреть файл

@ -1,18 +1,23 @@
import sys
import threading
from PySide2.QtCore import QObject, Signal, Slot, QTimer
from PySide2.QtCore import QObject, QTimer, Signal, Slot
from PySide2.QtWidgets import QMessageBox
from mozregression.bisector import Bisector, Bisection, NightlyHandler, \
IntegrationHandler, IndexPromise
from mozregression.errors import MozRegressionError
from mozregression.dates import is_date_or_datetime
from mozregression.approx_persist import ApproxPersistChooser
from mozregression.bisector import (
Bisection,
Bisector,
IndexPromise,
IntegrationHandler,
NightlyHandler,
)
from mozregression.config import DEFAULT_EXPAND
from mozregression.dates import is_date_or_datetime
from mozregression.errors import MozRegressionError
from mozregui.build_runner import AbstractBuildRunner
from mozregui.skip_chooser import SkipDialog
from mozregui.log_report import log
from mozregui.skip_chooser import SkipDialog
Bisection.EXCEPTION = -1 # new possible value of bisection end
@ -27,8 +32,7 @@ class GuiBisector(QObject, Bisector):
step_finished = Signal(object, str)
handle_merge = Signal(object, str, str, str)
def __init__(self, fetch_config, test_runner, download_manager,
download_in_background=True):
def __init__(self, fetch_config, test_runner, download_manager, download_in_background=True):
QObject.__init__(self)
Bisector.__init__(self, fetch_config, test_runner, download_manager)
self.bisection = None
@ -42,8 +46,7 @@ class GuiBisector(QObject, Bisector):
self._persist_files = ()
self.should_stop = threading.Event()
self.download_manager.download_finished.connect(
self._build_dl_finished)
self.download_manager.download_finished.connect(self._build_dl_finished)
self.test_runner.evaluate_finished.connect(self._evaluate_finished)
def _finish_on_exception(self, bisection):
@ -66,9 +69,14 @@ class GuiBisector(QObject, Bisector):
handler = self.bisection.handler
try:
nhandler = IntegrationHandler(find_fix=self.bisection.handler.find_fix)
Bisector.bisect(self, nhandler, handler.good_revision,
handler.bad_revision, expand=DEFAULT_EXPAND,
interrupt=self.should_stop.is_set)
Bisector.bisect(
self,
nhandler,
handler.good_revision,
handler.bad_revision,
expand=DEFAULT_EXPAND,
interrupt=self.should_stop.is_set,
)
except MozRegressionError:
self._finish_on_exception(None)
except StopIteration:
@ -89,20 +97,22 @@ class GuiBisector(QObject, Bisector):
self.handle_merge.emit(self.bisection, *result)
def _bisect(self, handler, build_range):
self.bisection = Bisection(handler, build_range,
self.download_manager,
self.test_runner,
dl_in_background=False,
approx_chooser=self.approx_chooser)
self.bisection = Bisection(
handler,
build_range,
self.download_manager,
self.test_runner,
dl_in_background=False,
approx_chooser=self.approx_chooser,
)
self._bisect_next()
@Slot()
def _bisect_next(self):
# this is executed in the working thread
if self.test_runner.verdict != 'r':
if self.test_runner.verdict != "r":
try:
self.mid = self.bisection.search_mid_point(
interrupt=self.should_stop.is_set)
self.mid = self.bisection.search_mid_point(interrupt=self.should_stop.is_set)
except MozRegressionError:
self._finish_on_exception(self.bisection)
return
@ -111,9 +121,11 @@ class GuiBisector(QObject, Bisector):
# if our last answer was skip, and that the next build
# to use is not chosen yet, ask to choose it.
if (self._next_build_index is None and
self.test_runner.verdict == 's' and
len(self.bisection.build_range) > 3):
if (
self._next_build_index is None
and self.test_runner.verdict == "s"
and len(self.bisection.build_range) > 3
):
self.choose_next_build.emit()
return
@ -133,8 +145,12 @@ class GuiBisector(QObject, Bisector):
self.finished.emit(self.bisection, result)
else:
self.build_infos = self.bisection.handler.build_range[self.mid]
found, self.mid, self.build_infos, self._persist_files = \
self.bisection._find_approx_build(self.mid, self.build_infos)
(
found,
self.mid,
self.build_infos,
self._persist_files,
) = self.bisection._find_approx_build(self.mid, self.build_infos)
if not found:
self.download_manager.focus_download(self.build_infos)
self.step_build_found.emit(self.bisection, self.build_infos)
@ -149,11 +165,9 @@ class GuiBisector(QObject, Bisector):
# download in background, if desired and that last verdict was not
# a skip.
if self.download_in_background and self.test_runner.verdict != 's':
if self.download_in_background and self.test_runner.verdict != "s":
self.index_promise = IndexPromise(
self.mid,
self.bisection._download_next_builds,
args=(self._persist_files,)
self.mid, self.bisection._download_next_builds, args=(self._persist_files,),
)
# run the build evaluation
self.bisection.evaluate(self.build_infos)
@ -188,8 +202,7 @@ class GuiBisector(QObject, Bisector):
self.index_promise = None
self.step_finished.emit(self.bisection, self.test_runner.verdict)
result = self.bisection.handle_verdict(self.mid,
self.test_runner.verdict)
result = self.bisection.handle_verdict(self.mid, self.test_runner.verdict)
if result != Bisection.RUNNING:
self.finished.emit(self.bisection, result)
else:
@ -208,17 +221,19 @@ class BisectRunner(AbstractBuildRunner):
self.worker.handle_merge.connect(self.handle_merge)
self.worker.choose_next_build.connect(self.choose_next_build)
good, bad = options.pop('good'), options.pop('bad')
if is_date_or_datetime(good) and is_date_or_datetime(bad) \
and fetch_config.should_use_archive():
handler = NightlyHandler(find_fix=options['find_fix'])
good, bad = options.pop("good"), options.pop("bad")
if (
is_date_or_datetime(good)
and is_date_or_datetime(bad)
and fetch_config.should_use_archive()
):
handler = NightlyHandler(find_fix=options["find_fix"])
else:
handler = IntegrationHandler(find_fix=options['find_fix'])
handler = IntegrationHandler(find_fix=options["find_fix"])
self.worker._bisect_args = (handler, good, bad)
self.worker.download_in_background = \
self.global_prefs['background_downloads']
if self.global_prefs['approx_policy']:
self.worker.download_in_background = self.global_prefs["background_downloads"]
if self.global_prefs["approx_policy"]:
self.worker.approx_chooser = ApproxPersistChooser(7)
return self.worker.bisect
@ -236,11 +251,12 @@ class BisectRunner(AbstractBuildRunner):
QMessageBox.warning(
self.mainwindow,
"Launcher Error",
("An error occured while starting the process, so the build"
" will be skipped. Error message:<br><strong>%s</strong>"
% err_message)
(
"An error occured while starting the process, so the build"
" will be skipped. Error message:<br><strong>%s</strong>" % err_message
),
)
self.worker.test_runner.finish('s')
self.worker.test_runner.finish("s")
@Slot(str)
def set_verdict(self, verdict):
@ -275,11 +291,10 @@ class BisectRunner(AbstractBuildRunner):
dialog = QMessageBox.critical
else:
fetch_config = self.worker.fetch_config
if not getattr(bisection, 'no_more_merge', False):
if not getattr(bisection, "no_more_merge", False):
if isinstance(bisection.handler, NightlyHandler):
handler = bisection.handler
fetch_config.set_repo(
fetch_config.get_nightly_repo(handler.bad_date))
fetch_config.set_repo(fetch_config.get_nightly_repo(handler.bad_date))
QTimer.singleShot(0, self.worker.bisect_further)
else:
# check merge, try to bisect further

Просмотреть файл

@ -1,13 +1,11 @@
from PySide2.QtCore import QObject, QThread, Signal, \
Slot, QTimer
from PySide2.QtCore import QObject, QThread, QTimer, Signal, Slot
from mozregui.global_prefs import get_prefs, apply_prefs
from mozregression.download_manager import BuildDownloadManager
from mozregression.test_runner import create_launcher
from mozregression.errors import LauncherError
from mozregression.network import get_http_session
from mozregression.persist_limit import PersistLimit
from mozregression.errors import LauncherError
from mozregression.test_runner import create_launcher
from mozregui.global_prefs import apply_prefs, get_prefs
from mozregui.log_report import log
@ -18,10 +16,9 @@ class GuiBuildDownloadManager(QObject, BuildDownloadManager):
def __init__(self, destdir, persist_limit, **kwargs):
QObject.__init__(self)
BuildDownloadManager.__init__(self, destdir,
session=get_http_session(),
persist_limit=persist_limit,
**kwargs)
BuildDownloadManager.__init__(
self, destdir, session=get_http_session(), persist_limit=persist_limit, **kwargs
)
def _download_started(self, task):
self.download_started.emit(task)
@ -72,7 +69,7 @@ class GuiTestRunner(QObject):
self.run_error = True
self.evaluate_started.emit(str(exc))
else:
self.evaluate_started.emit('')
self.evaluate_started.emit("")
self.run_error = False
def finish(self, verdict):
@ -94,6 +91,7 @@ class AbstractBuildRunner(QObject):
Create the required test runner and build manager, along with a thread
that should be used for blocking tasks.
"""
running_state_changed = Signal(bool)
worker_created = Signal(object)
worker_class = None
@ -124,42 +122,36 @@ class AbstractBuildRunner(QObject):
# apply the global prefs now
apply_prefs(global_prefs)
fetch_config.set_base_url(global_prefs['archive_base_url'])
fetch_config.set_base_url(global_prefs["archive_base_url"])
download_dir = global_prefs['persist']
download_dir = global_prefs["persist"]
if not download_dir:
download_dir = self.mainwindow.persist
persist_limit = PersistLimit(
abs(global_prefs['persist_size_limit']) * 1073741824
)
self.download_manager = GuiBuildDownloadManager(download_dir,
persist_limit)
persist_limit = PersistLimit(abs(global_prefs["persist_size_limit"]) * 1073741824)
self.download_manager = GuiBuildDownloadManager(download_dir, persist_limit)
self.test_runner = GuiTestRunner()
self.thread = QThread()
# options for the app launcher
launcher_kwargs = {}
for name in ('profile', 'preferences'):
for name in ("profile", "preferences"):
if name in options:
value = options[name]
if value:
launcher_kwargs[name] = value
# add add-ons paths to the app launcher
launcher_kwargs['addons'] = options['addons']
launcher_kwargs["addons"] = options["addons"]
self.test_runner.launcher_kwargs = launcher_kwargs
if options['profile_persistence'] in ('clone-first', 'reuse') or options['profile']:
launcher_kwargs['cmdargs'] = launcher_kwargs.get('cmdargs', []) + ['--allow-downgrade']
if options["profile_persistence"] in ("clone-first", "reuse") or options["profile"]:
launcher_kwargs["cmdargs"] = launcher_kwargs.get("cmdargs", []) + ["--allow-downgrade"]
# Thunderbird will fail to start if passed an URL arg
if 'url' in options and fetch_config.app_name != 'thunderbird':
launcher_kwargs['cmdargs'] = (
launcher_kwargs.get('cmdargs', []) + [options['url']]
)
if "url" in options and fetch_config.app_name != "thunderbird":
launcher_kwargs["cmdargs"] = launcher_kwargs.get("cmdargs", []) + [options["url"]]
self.worker = self.worker_class(fetch_config, self.test_runner,
self.download_manager)
self.worker = self.worker_class(fetch_config, self.test_runner, self.download_manager)
# Move self.bisector in the thread. This will
# allow to the self.bisector slots (connected after the move)
# to be automatically called in the thread.
@ -179,9 +171,8 @@ class AbstractBuildRunner(QObject):
def stop(self, wait=True):
self.stopped = True
if self.options:
if self.options['profile'] and \
self.options['profile_persistence'] == 'clone-first':
self.options['profile'].cleanup()
if self.options["profile"] and self.options["profile_persistence"] == "clone-first":
self.options["profile"].cleanup()
if self.download_manager:
self.download_manager.cancel()
if self.thread:
@ -206,7 +197,7 @@ class AbstractBuildRunner(QObject):
if self.test_runner:
self.test_runner.finish(None)
self.running_state_changed.emit(False)
log('Stopped')
log("Stopped")
@Slot()
def _remove_pending_thread(self):

Просмотреть файл

@ -1,15 +1,13 @@
from PySide2.QtCore import QObject, QThread, Slot, Qt, QUrl
from PySide2.QtCore import QObject, Qt, QThread, QUrl, Slot
from PySide2.QtGui import QDesktopServices
from PySide2.QtWidgets import QLabel
from mozregression.network import retry_get
from mozregression import __version__ as mozregression_version
from mozregression.network import retry_get
class CheckReleaseThread(QThread):
GITHUB_LATEST_RELEASE_URL = (
"https://api.github.com/repos/mozilla/mozregression/releases/latest"
)
GITHUB_LATEST_RELEASE_URL = "https://api.github.com/repos/mozilla/mozregression/releases/latest"
def __init__(self):
QThread.__init__(self)
@ -18,8 +16,8 @@ class CheckReleaseThread(QThread):
def run(self):
data = retry_get(self.GITHUB_LATEST_RELEASE_URL).json()
self.tag_name = data['tag_name']
self.release_url = data['html_url']
self.tag_name = data["tag_name"]
self.release_url = data["html_url"]
class CheckRelease(QObject):
@ -44,9 +42,9 @@ class CheckRelease(QObject):
return
self.label.setText(
'There is a new release available! Download the new'
' <a href="%s">release %s</a>.'
% (self.thread.release_url, release_name))
"There is a new release available! Download the new"
' <a href="%s">release %s</a>.' % (self.thread.release_url, release_name)
)
self.mainwindow.ui.status_bar.addWidget(self.label)
@Slot(str)

Просмотреть файл

@ -1,10 +1,12 @@
import platform
import sys
import traceback
import platform
from PySide2.QtCore import QObject, Qt, Signal, Slot
from PySide2.QtWidgets import QDialog
import mozregression
from PySide2.QtCore import QObject, Slot, Signal, Qt
from PySide2.QtWidgets import QDialog
from .ui.crash_reporter import Ui_CrashDialog
@ -23,15 +25,18 @@ traceback: %(traceback)s
self.ui.setupUi(self)
def set_exception(self, type, value, tb):
frozen = ' FROZEN' if getattr(sys, 'frozen', False) else ''
self.ui.information.setPlainText(self.ERR_TEMPLATE % dict(
mozregression=mozregression.__version__,
message="%s: %s" % (type.__name__, value),
traceback=''.join(traceback.format_tb(tb)) if tb else 'NONE',
platform=platform.platform(),
python=platform.python_version() + frozen,
arch=platform.architecture()[0],
))
frozen = " FROZEN" if getattr(sys, "frozen", False) else ""
self.ui.information.setPlainText(
self.ERR_TEMPLATE
% dict(
mozregression=mozregression.__version__,
message="%s: %s" % (type.__name__, value),
traceback="".join(traceback.format_tb(tb)) if tb else "NONE",
platform=platform.platform(),
python=platform.python_version() + frozen,
arch=platform.architecture()[0],
)
)
class CrashReporter(QObject):

Просмотреть файл

@ -1,12 +1,11 @@
import os
from configobj import ConfigObj
from PySide2.QtWidgets import QDialog
from mozregui.ui.global_prefs import Ui_GlobalPrefs
from mozregression.config import ARCHIVE_BASE_URL, DEFAULT_CONF_FNAME, get_defaults
from mozregression.network import set_http_session
from mozregression.config import (DEFAULT_CONF_FNAME, get_defaults,
ARCHIVE_BASE_URL)
from configobj import ConfigObj
from mozregui.ui.global_prefs import Ui_GlobalPrefs
def get_prefs():
@ -15,14 +14,15 @@ def get_prefs():
"""
settings = get_defaults(DEFAULT_CONF_FNAME)
options = dict()
options['persist'] = settings['persist']
options['http_timeout'] = float(settings['http-timeout'])
options['persist_size_limit'] = float(settings['persist-size-limit'])
options['background_downloads'] = \
False if settings.get('background_downloads') == 'no' else True
options['approx_policy'] = settings['approx-policy'] == 'auto'
options['archive_base_url'] = settings["archive-base-url"]
options['cmdargs'] = settings['cmdargs']
options["persist"] = settings["persist"]
options["http_timeout"] = float(settings["http-timeout"])
options["persist_size_limit"] = float(settings["persist-size-limit"])
options["background_downloads"] = (
False if settings.get("background_downloads") == "no" else True
)
options["approx_policy"] = settings["approx-policy"] == "auto"
options["archive_base_url"] = settings["archive-base-url"]
options["cmdargs"] = settings["cmdargs"]
return options
@ -31,23 +31,23 @@ def save_prefs(options):
if not os.path.isdir(conf_dir):
os.makedirs(conf_dir)
settings = ConfigObj(DEFAULT_CONF_FNAME)
settings.update({
'persist': options['persist'] or '',
'http-timeout': options['http_timeout'],
'persist-size-limit': options['persist_size_limit'],
'background_downloads':
'yes' if options['background_downloads'] else 'no',
'approx-policy': 'auto' if options['approx_policy'] else 'none',
})
settings.update(
{
"persist": options["persist"] or "",
"http-timeout": options["http_timeout"],
"persist-size-limit": options["persist_size_limit"],
"background_downloads": "yes" if options["background_downloads"] else "no",
"approx-policy": "auto" if options["approx_policy"] else "none",
}
)
# only save base url in the file if it differs from the default.
if options['archive_base_url'] and \
options['archive_base_url'] != ARCHIVE_BASE_URL:
settings['archive-base-url'] = options['archive_base_url']
elif 'archive-base-url' in settings:
del settings['archive-base-url']
if options["archive_base_url"] and options["archive_base_url"] != ARCHIVE_BASE_URL:
settings["archive-base-url"] = options["archive_base_url"]
elif "archive-base-url" in settings:
del settings["archive-base-url"]
# likewise only save args if it has a value
if 'cmdargs' in settings and not settings['cmdargs']:
del settings['cmdargs']
if "cmdargs" in settings and not settings["cmdargs"]:
del settings["cmdargs"]
settings.write()
@ -55,16 +55,13 @@ def set_default_prefs():
"""Set the default prefs for a first launch of the application."""
if not os.path.isfile(DEFAULT_CONF_FNAME):
options = get_prefs()
options["persist"] = os.path.join(os.path.dirname(DEFAULT_CONF_FNAME),
"persist")
options["persist"] = os.path.join(os.path.dirname(DEFAULT_CONF_FNAME), "persist")
options["persist_size_limit"] = 2.0
save_prefs(options)
def apply_prefs(options):
set_http_session(get_defaults={
"timeout": options['http_timeout'],
})
set_http_session(get_defaults={"timeout": options["http_timeout"]})
# persist options have to be passed in the bisection, not handled here.
@ -76,12 +73,12 @@ class ChangePrefsDialog(QDialog):
# set default values
options = get_prefs()
self.ui.persist.line_edit.setText(options['persist'] or '')
self.ui.http_timeout.setValue(options['http_timeout'])
self.ui.persist_size_limit.setValue(options['persist_size_limit'])
self.ui.bg_downloads.setChecked(options['background_downloads'])
self.ui.approx.setChecked(options['approx_policy'])
self.ui.archive_base_url.setText(options['archive_base_url'])
self.ui.persist.line_edit.setText(options["persist"] or "")
self.ui.http_timeout.setValue(options["http_timeout"])
self.ui.persist_size_limit.setValue(options["persist_size_limit"])
self.ui.bg_downloads.setChecked(options["background_downloads"])
self.ui.approx.setChecked(options["approx_policy"])
self.ui.archive_base_url.setText(options["archive_base_url"])
self.ui.advanced_options.setText("Show Advanced Options")
self.toggle_visibility(False)
self.ui.advanced_options.clicked.connect(self.toggle_adv_options)
@ -106,12 +103,12 @@ class ChangePrefsDialog(QDialog):
options = get_prefs()
ui = self.ui
options['persist'] = str(ui.persist.line_edit.text()) or None
options['http_timeout'] = ui.http_timeout.value()
options['persist_size_limit'] = ui.persist_size_limit.value()
options['background_downloads'] = ui.bg_downloads.isChecked()
options['approx_policy'] = ui.approx.isChecked()
options['archive_base_url'] = str(ui.archive_base_url.text())
options["persist"] = str(ui.persist.line_edit.text()) or None
options["http_timeout"] = ui.http_timeout.value()
options["persist_size_limit"] = ui.persist_size_limit.value()
options["background_downloads"] = ui.bg_downloads.isChecked()
options["approx_policy"] = ui.approx.isChecked()
options["archive_base_url"] = str(ui.archive_base_url.text())
save_prefs(options)

Просмотреть файл

@ -1,19 +1,17 @@
from PySide2.QtCore import (QObject, Slot, Signal)
from PySide2.QtWidgets import (QAction, QActionGroup, QMenu, QPlainTextEdit)
from PySide2.QtGui import (QTextCursor, QColor,
QTextCharFormat,
QTextBlockUserData)
from datetime import datetime
from mozlog.structuredlog import log_levels
from mozlog import get_default_logger
from mozlog.structuredlog import log_levels
from PySide2.QtCore import QObject, Signal, Slot
from PySide2.QtGui import QColor, QTextBlockUserData, QTextCharFormat, QTextCursor
from PySide2.QtWidgets import QAction, QActionGroup, QMenu, QPlainTextEdit
COLORS = {
'DEBUG': QColor(6, 146, 6), # green
'INFO': QColor(250, 184, 4), # deep yellow
'WARNING': QColor(255, 0, 0, 127), # red
'CRITICAL': QColor(255, 0, 0, 127),
'ERROR': QColor(255, 0, 0, 127),
"DEBUG": QColor(6, 146, 6), # green
"INFO": QColor(250, 184, 4), # deep yellow
"WARNING": QColor(255, 0, 0, 127), # red
"CRITICAL": QColor(255, 0, 0, 127),
"ERROR": QColor(255, 0, 0, 127),
}
@ -29,16 +27,17 @@ class LogView(QPlainTextEdit):
self.setMaximumBlockCount(1000)
self.group = QActionGroup(self)
self.actions = [QAction(log_lvl, self.group) for log_lvl in
["Debug", "Info", "Warning", "Error", "Critical"]]
self.actions = [
QAction(log_lvl, self.group)
for log_lvl in ["Debug", "Info", "Warning", "Error", "Critical"]
]
for action in self.actions:
action.setCheckable(True)
action.triggered.connect(self.on_log_filter)
self.actions[0].setChecked(True)
self.customContextMenuRequested.connect(
self.on_custom_context_menu_requested)
self.customContextMenuRequested.connect(self.on_custom_context_menu_requested)
self.log_lvl = log_levels["INFO"]
@ -50,24 +49,22 @@ class LogView(QPlainTextEdit):
@Slot(dict)
def on_log_received(self, data):
time_info = datetime.fromtimestamp((data['time'] / 1000)).isoformat()
log_message = '%s: %s : %s' % (
time_info, data['level'], data['message'])
time_info = datetime.fromtimestamp((data["time"] / 1000)).isoformat()
log_message = "%s: %s : %s" % (time_info, data["level"], data["message"])
message_document = self.document()
cursor_to_add = QTextCursor(message_document)
cursor_to_add.movePosition(cursor_to_add.End)
cursor_to_add.insertText(log_message + '\n')
cursor_to_add.insertText(log_message + "\n")
if data['level'] in COLORS:
if data["level"] in COLORS:
fmt = QTextCharFormat()
fmt.setForeground(COLORS[data['level']])
fmt.setForeground(COLORS[data["level"]])
cursor_to_add.movePosition(cursor_to_add.PreviousBlock)
log_lvl_data = LogLevelData(log_levels[data['level'].upper()])
log_lvl_data = LogLevelData(log_levels[data["level"].upper()])
cursor_to_add.block().setUserData(log_lvl_data)
cursor_to_add_fmt = message_document.find(data['level'],
cursor_to_add.position())
cursor_to_add_fmt = message_document.find(data["level"], cursor_to_add.position())
cursor_to_add_fmt.mergeCharFormat(fmt)
if log_levels[data['level']] > self.log_lvl:
if log_levels[data["level"]] > self.log_lvl:
cursor_to_add.block().setVisible(False)
self.ensureCursorVisible()
@ -103,11 +100,12 @@ class LogModel(QObject):
def log(text, log=True, status_bar=True, status_bar_timeout=2.0):
if log:
logger = get_default_logger('mozregui')
logger = get_default_logger("mozregui")
if logger:
logger.info(text)
if status_bar:
from mozregui.mainwindow import MainWindow
mw = MainWindow.INSTANCE
if mw:
mw.ui.status_bar.showMessage(text, int(status_bar_timeout * 1000))

Просмотреть файл

@ -1,17 +1,17 @@
import sys
from mozlog.structuredlog import StructuredLogger, set_default_logger
from PySide2.QtWidgets import QApplication
from mozlog.structuredlog import set_default_logger, StructuredLogger
from .log_report import LogModel
from .check_release import CheckRelease
from .crash_reporter import CrashReporter
from .mainwindow import MainWindow
from .global_prefs import set_default_prefs
from .log_report import LogModel
from .mainwindow import MainWindow
def main():
logger = StructuredLogger('mozregression-gui')
logger = StructuredLogger("mozregression-gui")
set_default_logger(logger)
# Create a Qt application
log_model = LogModel()
@ -20,9 +20,9 @@ def main():
app = QApplication(argv)
crash_reporter = CrashReporter(app)
crash_reporter.install()
app.setOrganizationName('mozilla')
app.setOrganizationDomain('mozilla.org')
app.setApplicationName('mozregression-gui')
app.setOrganizationName("mozilla")
app.setOrganizationDomain("mozilla.org")
app.setApplicationName("mozregression-gui")
set_default_prefs()
# Create the main window and show it
win = MainWindow()
@ -37,5 +37,5 @@ def main():
sys.exit(app.exec_())
if __name__ == '__main__':
if __name__ == "__main__":
main()

Просмотреть файл

@ -1,18 +1,16 @@
import mozregression
import mozfile
from tempfile import mkdtemp
from PySide2.QtCore import Slot, QSettings
import mozfile
from PySide2.QtCore import QSettings, Slot
from PySide2.QtWidgets import QMainWindow, QMessageBox
from mozregui.ui.mainwindow import Ui_MainWindow
from mozregui.wizard import BisectionWizard, SingleRunWizard
import mozregression
from mozregui.bisection import BisectRunner
from mozregui.single_runner import SingleBuildRunner
from mozregui.global_prefs import change_prefs_dialog
from mozregui.report_delegate import ReportItemDelegate
from mozregui.single_runner import SingleBuildRunner
from mozregui.ui.mainwindow import Ui_MainWindow
from mozregui.wizard import BisectionWizard, SingleRunWizard
ABOUT_TEXT = """\
<p><strong>mozregression-gui</strong> is a desktop interface for
@ -27,7 +25,9 @@ http://mozilla.github.io/mozregression/</a></p>
from <a href="http://www.flaticon.com" title="Flaticon">www.flaticon.com</a>
and licensed under <a href="http://creativecommons.org/licenses/by/3.0/"
title="Creative Commons BY 3.0">CC BY 3.0</a></p>
""" % (mozregression.__version__)
""" % (
mozregression.__version__
)
class MainWindow(QMainWindow):
@ -43,29 +43,22 @@ class MainWindow(QMainWindow):
self.single_runner = SingleBuildRunner(self)
self.current_runner = None
self.bisect_runner.worker_created.connect(
self.ui.report_view.model().attach_bisector)
self.single_runner.worker_created.connect(
self.ui.report_view.model().attach_single_runner)
self.bisect_runner.worker_created.connect(self.ui.report_view.model().attach_bisector)
self.single_runner.worker_created.connect(self.ui.report_view.model().attach_single_runner)
self.ui.report_view.model().need_evaluate_editor.connect(
self.bisect_runner.open_evaluate_editor)
self.ui.report_view.step_report_changed.connect(
self.ui.build_info_browser.update_content)
self.report_delegate = ReportItemDelegate()
self.report_delegate.got_verdict.connect(
self.bisect_runner.set_verdict
self.bisect_runner.open_evaluate_editor
)
self.ui.report_view.step_report_changed.connect(self.ui.build_info_browser.update_content)
self.report_delegate = ReportItemDelegate()
self.report_delegate.got_verdict.connect(self.bisect_runner.set_verdict)
self.ui.report_view.setItemDelegateForColumn(0, self.report_delegate)
for runner in (self.bisect_runner, self.single_runner):
runner.running_state_changed.connect(
self.ui.actionStart_a_new_bisection.setDisabled)
runner.running_state_changed.connect(
self.ui.actionStop_the_bisection.setEnabled)
runner.running_state_changed.connect(
self.ui.actionRun_a_single_build.setDisabled)
runner.running_state_changed.connect(self.ui.actionStart_a_new_bisection.setDisabled)
runner.running_state_changed.connect(self.ui.actionStop_the_bisection.setEnabled)
runner.running_state_changed.connect(self.ui.actionRun_a_single_build.setDisabled)
self.persist = mkdtemp()

Просмотреть файл

@ -1,8 +1,8 @@
import sys
import os
import sys
# only True when running from frozen (bundled) app
IS_FROZEN = getattr(sys, 'frozen', False)
IS_FROZEN = getattr(sys, "frozen", False)
def cacert_path():
@ -15,11 +15,12 @@ def patch():
# patch requests.request so taskcluster can use the right cacert.pem file.
if IS_FROZEN:
import requests
pem = cacert_path()
old_request = requests.request
def _patched_request(*args, **kwargs):
kwargs['verify'] = pem
kwargs["verify"] = pem
return old_request(*args, **kwargs)
requests.request = _patched_request

Просмотреть файл

@ -1,7 +1,6 @@
from PySide2.QtCore import QAbstractTableModel, QModelIndex, Qt, \
Slot
from PySide2.QtWidgets import QFileDialog, QWidget
from mozprofile.prefs import Preferences
from PySide2.QtCore import QAbstractTableModel, QModelIndex, Qt, Slot
from PySide2.QtWidgets import QFileDialog, QWidget
from mozregui.ui.pref_editor import Ui_PrefEditor
@ -10,6 +9,7 @@ class PreferencesModel(QAbstractTableModel):
"""
A Qt model that can edit preferences.
"""
def __init__(self):
QAbstractTableModel.__init__(self)
self.prefs = []
@ -22,7 +22,7 @@ class PreferencesModel(QAbstractTableModel):
def headerData(self, section, orientation, role=Qt.DisplayRole):
if role == Qt.DisplayRole and orientation == Qt.Horizontal:
return ('name', 'value')[section]
return ("name", "value")[section]
def data(self, index, role=Qt.DisplayRole):
if role in (Qt.DisplayRole, Qt.EditRole):
@ -52,15 +52,14 @@ class PreferencesModel(QAbstractTableModel):
def add_empty_pref(self):
nb_prefs = len(self.prefs)
self.beginInsertRows(QModelIndex(), nb_prefs, nb_prefs)
self.prefs.append(('', ''))
self.prefs.append(("", ""))
self.endInsertRows()
def add_prefs_from_file(self, fname):
prefs = Preferences.read(fname)
if prefs:
nb_prefs = len(self.prefs)
self.beginInsertRows(QModelIndex(), nb_prefs,
nb_prefs + len(prefs) - 1)
self.beginInsertRows(QModelIndex(), nb_prefs, nb_prefs + len(prefs) - 1)
self.prefs.extend(list(prefs.items()))
self.endInsertRows()
@ -75,6 +74,7 @@ class PreferencesWidgetEditor(QWidget):
A widget to edit preferences, using PreferencesModel in a table view
and buttons to let the user interact.
"""
def __init__(self, parent=None):
QWidget.__init__(self, parent)
self.ui = Ui_PrefEditor()
@ -93,9 +93,7 @@ class PreferencesWidgetEditor(QWidget):
@Slot()
def add_prefs_from_file(self):
(fileName, _) = QFileDialog.getOpenFileName(
self,
"Choose a preference file",
filter="pref file (*.json *.ini)",
self, "Choose a preference file", filter="pref file (*.json *.ini)",
)
if fileName:
self.pref_model.add_prefs_from_file(fileName)
@ -103,8 +101,7 @@ class PreferencesWidgetEditor(QWidget):
@Slot()
def remove_selected_prefs(self):
selected_rows = sorted(
set(i.row() for i in self.ui.pref_view.selectedIndexes()),
reverse=True
set(i.row() for i in self.ui.pref_view.selectedIndexes()), reverse=True
)
for row in selected_rows:
self.pref_model.remove_pref(row)
@ -113,7 +110,7 @@ class PreferencesWidgetEditor(QWidget):
return self.pref_model.prefs[:]
if __name__ == '__main__':
if __name__ == "__main__":
from PySide2.QtWidgets import QApplication
app = QApplication([])

Просмотреть файл

@ -1,6 +1,5 @@
from PySide2.QtCore import (QAbstractTableModel, QModelIndex, Qt,
Slot, Signal, QUrl)
from PySide2.QtGui import QDesktopServices, QColor
from PySide2.QtCore import QAbstractTableModel, QModelIndex, Qt, QUrl, Signal, Slot
from PySide2.QtGui import QColor, QDesktopServices
from PySide2.QtWidgets import QTableView, QTextBrowser
from mozregression.bisector import NightlyHandler
@ -8,10 +7,10 @@ from mozregression.bisector import NightlyHandler
# Custom colors
GRAY_WHITE = QColor(243, 243, 243)
VERDICT_TO_ROW_COLORS = {
"g": QColor(152, 251, 152), # light green
"g": QColor(152, 251, 152), # light green
"b": QColor(250, 113, 113), # light red
"s": QColor(253, 248, 107), # light yellow
"r": QColor(225, 225, 225), # light gray
"s": QColor(253, 248, 107), # light yellow
"r": QColor(225, 225, 225), # light gray
}
@ -19,6 +18,7 @@ class ReportItem(object):
"""
A base item in the report view
"""
def __init__(self):
self.data = {}
self.downloading = False
@ -27,10 +27,10 @@ class ReportItem(object):
def update_pushlogurl(self, bisection):
if bisection.handler.found_repo:
self.data['pushlog_url'] = bisection.handler.get_pushlog_url()
self.data["pushlog_url"] = bisection.handler.get_pushlog_url()
else:
self.data['pushlog_url'] = 'Not available'
self.data['repo_name'] = bisection.build_range[0].repo_name
self.data["pushlog_url"] = "Not available"
self.data["repo_name"] = bisection.build_range[0].repo_name
def status_text(self):
return "Looking for build data..."
@ -43,6 +43,7 @@ class StartItem(ReportItem):
"""
Report a started bisection
"""
def update_pushlogurl(self, bisection):
ReportItem.update_pushlogurl(self, bisection)
handler = bisection.handler
@ -50,7 +51,7 @@ class StartItem(ReportItem):
self.build_type = "nightly"
else:
self.build_type = "integration"
if self.build_type == 'nightly':
if self.build_type == "nightly":
self.first, self.last = handler.get_date_range()
else:
self.first, self.last = handler.get_range()
@ -60,32 +61,31 @@ class StartItem(ReportItem):
self.first, self.last = self.last, self.first
def status_text(self):
if 'pushlog_url' not in self.data:
if "pushlog_url" not in self.data:
return ReportItem.status_text(self)
return 'Bisecting on %s [%s - %s]' % (self.data['repo_name'],
self.first, self.last)
return "Bisecting on %s [%s - %s]" % (self.data["repo_name"], self.first, self.last,)
class StepItem(ReportItem):
"""
Report a bisection step
"""
def __init__(self):
ReportItem.__init__(self)
self.state_text = 'Found'
self.state_text = "Found"
self.verdict = None
def status_text(self):
if not self.data:
return ReportItem.status_text(self)
if self.data['build_type'] == 'nightly':
desc = self.data['build_date']
if self.data["build_type"] == "nightly":
desc = self.data["build_date"]
else:
desc = self.data['changeset'][:8]
desc = self.data["changeset"][:8]
if self.verdict is not None:
desc = '%s (verdict: %s)' % (desc, self.verdict)
return "%s %s build: %s" % (self.state_text, self.data['repo_name'],
desc)
desc = "%s (verdict: %s)" % (desc, self.verdict)
return "%s %s build: %s" % (self.state_text, self.data["repo_name"], desc)
def _bulk_action_slots(action, slots, signal_object, slot_object):
@ -111,44 +111,39 @@ class ReportModel(QAbstractTableModel):
@Slot(object)
def attach_bisector(self, bisector):
bisector_slots = ('step_started',
'step_build_found',
'step_testing',
'step_finished',
'started',
'finished')
downloader_slots = ('download_progress', )
bisector_slots = (
"step_started",
"step_build_found",
"step_testing",
"step_finished",
"started",
"finished",
)
downloader_slots = ("download_progress",)
if bisector:
self.attach_single_runner(None)
_bulk_action_slots('connect',
bisector_slots,
bisector,
self)
_bulk_action_slots('connect',
downloader_slots,
bisector.download_manager,
self)
_bulk_action_slots("connect", bisector_slots, bisector, self)
_bulk_action_slots("connect", downloader_slots, bisector.download_manager, self)
self.bisector = bisector
@Slot(object)
def attach_single_runner(self, single_runner):
sr_slots = ('started', 'step_build_found', 'step_testing')
downloader_slots = ('download_progress', )
sr_slots = ("started", "step_build_found", "step_testing")
downloader_slots = ("download_progress",)
if single_runner:
self.attach_bisector(None)
_bulk_action_slots('connect', sr_slots, single_runner, self)
_bulk_action_slots('connect', downloader_slots,
single_runner.download_manager, self)
_bulk_action_slots("connect", sr_slots, single_runner, self)
_bulk_action_slots("connect", downloader_slots, single_runner.download_manager, self)
self.single_runner = single_runner
@Slot(object, int, int)
def download_progress(self, dl, current, total):
item = self.items[-1]
item.state_text = 'Downloading'
item.state_text = "Downloading"
item.downloading = True
item.set_progress(current, total)
self.update_item(item)
@ -168,9 +163,7 @@ class ReportModel(QAbstractTableModel):
return item.status_text()
elif role == Qt.BackgroundRole:
if isinstance(item, StepItem) and item.verdict:
return VERDICT_TO_ROW_COLORS.get(
str(item.verdict),
GRAY_WHITE)
return VERDICT_TO_ROW_COLORS.get(str(item.verdict), GRAY_WHITE)
else:
return GRAY_WHITE
@ -196,7 +189,7 @@ class ReportModel(QAbstractTableModel):
last_item = self.items[-1]
if isinstance(last_item, StepItem):
# update the pushlog for the start step
if hasattr(bisection, 'handler'):
if hasattr(bisection, "handler"):
last_item.update_pushlogurl(bisection)
self.update_item(last_item)
# and add a new step
@ -208,7 +201,7 @@ class ReportModel(QAbstractTableModel):
if isinstance(last_item, StartItem):
# update the pushlog for the start step
if hasattr(bisection, 'handler'):
if hasattr(bisection, "handler"):
last_item.update_pushlogurl(bisection)
self.update_item(last_item)
else:
@ -230,14 +223,14 @@ class ReportModel(QAbstractTableModel):
last_item = self.items[-1]
last_item.downloading = False
last_item.waiting_evaluation = True
last_item.state_text = 'Testing'
last_item.state_text = "Testing"
# we may have more build data now that the build has been installed
last_item.data.update(build_infos.to_dict())
if hasattr(bisection, 'handler'):
if hasattr(bisection, "handler"):
last_item.update_pushlogurl(bisection)
self.update_item(last_item)
if hasattr(bisection, 'handler'):
if hasattr(bisection, "handler"):
# not a single runner
index = self.createIndex(self.rowCount() - 1, 0)
self.need_evaluate_editor.emit(True, index)
@ -247,10 +240,10 @@ class ReportModel(QAbstractTableModel):
# step finished, just store the verdict
item = self.items[-1]
item.waiting_evaluation = False
item.state_text = 'Tested'
item.state_text = "Tested"
item.verdict = verdict
self.update_item(item)
if hasattr(bisection, 'handler'):
if hasattr(bisection, "handler"):
# not a single runner
index = self.createIndex(self.rowCount() - 1, 0)
self.need_evaluate_editor.emit(False, index)
@ -307,12 +300,12 @@ class BuildInfoTextBrowser(QTextBrowser):
for k in sorted(item.data):
v = item.data[k]
if v is not None:
html += '<strong>%s</strong>: ' % k
html += "<strong>%s</strong>: " % k
if isinstance(v, str):
url = QUrl(v)
if url.isValid() and url.scheme():
v = '<a href="%s">%s</a>' % (v, v)
html += '{}<br>'.format(v)
html += "{}<br>".format(v)
self.setHtml(html)
@Slot(QUrl)

Просмотреть файл

@ -1,10 +1,15 @@
from PySide2.QtCore import QRect, Qt, Signal
from PySide2.QtGui import QIcon, QPainter, QPixmap
from PySide2.QtWidgets import QStyledItemDelegate, QStyleOptionProgressBar, \
QApplication, QStyle, QWidget
from PySide2.QtCore import Qt, QRect, Signal
from PySide2.QtWidgets import (
QApplication,
QStyle,
QStyledItemDelegate,
QStyleOptionProgressBar,
QWidget,
)
from mozregui.ui.ask_verdict import Ui_AskVerdict
from mozregui.report import VERDICT_TO_ROW_COLORS
from mozregui.ui.ask_verdict import Ui_AskVerdict
VERDICTS = ("good", "bad", "skip", "retry", "other...")
@ -33,7 +38,7 @@ class AskVerdict(QWidget):
AskVerdict.icons_cache[text] = QIcon(pixmap)
# set combo verdict
for text in ('other...', 'skip', 'retry'):
for text in ("other...", "skip", "retry"):
self.ui.comboVerdict.addItem(AskVerdict.icons_cache[text], text)
model = self.ui.comboVerdict.model()
model.itemFromIndex(model.index(0, 0)).setSelectable(False)
@ -47,15 +52,11 @@ class AskVerdict(QWidget):
self.ui.badVerdict.setIcon(AskVerdict.icons_cache["bad"])
def on_dropdown_item_activated(self):
self.delegate.got_verdict.emit(
str(self.ui.comboVerdict.currentText())[0]
)
self.delegate.got_verdict.emit(str(self.ui.comboVerdict.currentText())[0])
self.emitted = True
def on_good_bad_button_clicked(self):
self.delegate.got_verdict.emit(
str(self.sender().text())[0]
)
self.delegate.got_verdict.emit(str(self.sender().text())[0])
self.emitted = True
@ -69,14 +70,13 @@ class ReportItemDelegate(QStyledItemDelegate):
if index.model().get_item(index).waiting_evaluation:
return AskVerdict(parent, self)
else:
return QStyledItemDelegate.createEditor(self, parent, option,
index)
return QStyledItemDelegate.createEditor(self, parent, option, index)
def paint(self, painter, option, index):
# if item selected, override default theme
# Keeps verdict color for cells and use a bold font
if option.state & QStyle.State_Selected:
option.state &= ~ QStyle.State_Selected
option.state &= ~QStyle.State_Selected
option.font.setBold(True)
QStyledItemDelegate.paint(self, painter, option, index)
@ -88,17 +88,14 @@ class ReportItemDelegate(QStyledItemDelegate):
progressBarHeight = option.rect.height() / 4
progressBarOption.rect = QRect(
option.rect.x(),
option.rect.y() +
(option.rect.height() - progressBarHeight),
option.rect.y() + (option.rect.height() - progressBarHeight),
option.rect.width(),
progressBarHeight)
progressBarHeight,
)
progressBarOption.minimum = 0
progressBarOption.maximum = 100
progressBarOption.textAlignment = Qt.AlignCenter
progressBarOption.progress = item.progress
QApplication.style().drawControl(
QStyle.CE_ProgressBar,
progressBarOption,
painter)
QApplication.style().drawControl(QStyle.CE_ProgressBar, progressBarOption, painter)

Просмотреть файл

@ -1,11 +1,9 @@
from PySide2.QtCore import QObject, Slot, Signal
from PySide2.QtCore import QObject, Signal, Slot
from PySide2.QtWidgets import QMessageBox
from mozregression.errors import MozRegressionError
from mozregression.dates import is_date_or_datetime
from mozregression.fetch_build_info import (NightlyInfoFetcher,
IntegrationInfoFetcher)
from mozregression.errors import MozRegressionError
from mozregression.fetch_build_info import IntegrationInfoFetcher, NightlyInfoFetcher
from mozregui.build_runner import AbstractBuildRunner
@ -58,14 +56,12 @@ class SingleBuildRunner(AbstractBuildRunner):
def init_worker(self, fetch_config, options):
AbstractBuildRunner.init_worker(self, fetch_config, options)
self.download_manager.download_finished.connect(
self.worker._on_downloaded)
self.worker.launch_arg = options.pop('launch')
self.download_manager.download_finished.connect(self.worker._on_downloaded)
self.worker.launch_arg = options.pop("launch")
# evaluate_started will be called if we have an error
self.test_runner.evaluate_started.connect(self.on_error)
self.worker.error.connect(self.on_error)
if is_date_or_datetime(self.worker.launch_arg) and \
fetch_config.should_use_archive():
if is_date_or_datetime(self.worker.launch_arg) and fetch_config.should_use_archive():
return self.worker.launch_nightlies
else:
return self.worker.launch_integration

Просмотреть файл

@ -1,7 +1,13 @@
from PySide2.QtCore import Qt, Signal
from PySide2.QtGui import QBrush
from PySide2.QtWidgets import (QGraphicsRectItem, QGraphicsScene, QGraphicsView,
QToolTip, QDialog, QMessageBox)
from PySide2.QtWidgets import (
QDialog,
QGraphicsRectItem,
QGraphicsScene,
QGraphicsView,
QMessageBox,
QToolTip,
)
class BuildItem(QGraphicsRectItem):
@ -40,7 +46,7 @@ class SkipChooserScene(QGraphicsScene):
future,
column * BuildItem.WIDTH + self.SPACE * column,
row * BuildItem.WIDTH + self.SPACE * row,
selectable=i not in bounds
selectable=i not in bounds,
)
if i == mid:
item.setBrush(QBrush(Qt.blue))
@ -89,6 +95,7 @@ class SkipDialog(QDialog):
QDialog.__init__(self, parent)
assert len(build_range) > 3
from mozregui.ui.skip_dialog import Ui_SkipDialog
self.ui = Ui_SkipDialog()
self.ui.setupUi(self)
self.scene = SkipChooserScene(build_range)
@ -102,9 +109,7 @@ class SkipDialog(QDialog):
self.ui.lbl_status.setText("Selected build to test, %s" % items[0])
def build_index(self, item):
return self.scene.build_range.future_build_infos.index(
item.future_build_info
)
return self.scene.build_range.future_build_infos.index(item.future_build_info)
def choose_next_build(self):
if self.exec_() == self.Accepted:
@ -113,18 +118,23 @@ class SkipDialog(QDialog):
return self.build_index(items[0])
def closeEvent(self, evt):
if QMessageBox.warning(
self, "Stop the bisection ?",
if (
QMessageBox.warning(
self,
"Stop the bisection ?",
"Closing this dialog will end the bisection. Are you sure"
" you want to end the bisection now ?",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No) == QMessageBox.Yes:
QMessageBox.No,
)
== QMessageBox.Yes
):
evt.accept()
else:
evt.ignore()
if __name__ == '__main__':
if __name__ == "__main__":
from mozregression.build_range import BuildRange, FutureBuildInfo
class FInfo(FutureBuildInfo):
@ -134,6 +144,7 @@ if __name__ == '__main__':
build_range = BuildRange(None, [FInfo(None, i) for i in range(420)])
from PySide2.QtWidgets import QApplication, QMainWindow
app = QApplication([])
win = QMainWindow()

Просмотреть файл

@ -1,11 +1,17 @@
from PySide2.QtCore import QDir
from PySide2.QtWidgets import (QCompleter, QLineEdit, QPushButton,
QHBoxLayout, QFileDialog,
QFileSystemModel, QWidget)
from PySide2.QtWidgets import (
QCompleter,
QFileDialog,
QFileSystemModel,
QHBoxLayout,
QLineEdit,
QPushButton,
QWidget,
)
from mozregression.releases import date_of_release, releases
from mozregression.dates import parse_date
from mozregression.errors import DateFormatError
from mozregression.releases import date_of_release, releases
from mozregui.ui.build_selection_helper import Ui_BuildSelectionHelper
@ -13,6 +19,7 @@ class FSLineEdit(QLineEdit):
"""
A line edit with auto completion for file system folders.
"""
def __init__(self, parent=None):
QLineEdit.__init__(self, parent)
self.fsmodel = QFileSystemModel()
@ -20,8 +27,7 @@ class FSLineEdit(QLineEdit):
self.completer = QCompleter()
self.completer.setModel(self.fsmodel)
self.setCompleter(self.completer)
self.fsmodel.setFilter(QDir.Drives | QDir.AllDirs | QDir.Hidden |
QDir.NoDotAndDotDot)
self.fsmodel.setFilter(QDir.Drives | QDir.AllDirs | QDir.Hidden | QDir.NoDotAndDotDot)
def setPath(self, path):
self.setText(path)
@ -33,6 +39,7 @@ class DirectorySelectWidget(QWidget):
A FSLineEdit with a "browse" button on the right. Allow to select a
directory.
"""
def __init__(self, parent=None):
QWidget.__init__(self, parent)
layout = QHBoxLayout(self)
@ -46,9 +53,7 @@ class DirectorySelectWidget(QWidget):
self.button.clicked.connect(self.browse_dialog)
def browse_dialog(self):
path = QFileDialog.getExistingDirectory(
self, "Find file"
)
path = QFileDialog.getExistingDirectory(self, "Find file")
if path:
self.line_edit.setPath(path)
@ -58,21 +63,20 @@ class BuildSelection(QWidget):
Allow to select a date, a build id, a release number or an arbitrary
changeset.
"""
def __init__(self, parent=None):
QWidget.__init__(self, parent)
self.ui = Ui_BuildSelectionHelper()
self.ui.setupUi(self)
self.ui.release.addItems([str(k) for k in sorted(releases())])
self.ui.combo_helper.currentIndexChanged.connect(
self.ui.stackedWidget.setCurrentIndex)
self.ui.combo_helper.currentIndexChanged.connect(self.ui.stackedWidget.setCurrentIndex)
def get_value(self):
currentw = self.ui.stackedWidget.currentWidget()
if currentw == self.ui.s_date:
return self.ui.date.date().toPython()
elif currentw == self.ui.s_release:
return parse_date(
date_of_release(str(self.ui.release.currentText())))
return parse_date(date_of_release(str(self.ui.release.currentText())))
elif currentw == self.ui.s_buildid:
buildid = self.ui.buildid.text()
try:

Просмотреть файл

@ -1,23 +1,25 @@
from __future__ import absolute_import
import mozinfo
import datetime
from PySide2.QtWidgets import (QApplication, QCompleter, QWizard, QWizardPage, QMessageBox)
from PySide2.QtCore import QStringListModel, QDate, Slot, Qt, SIGNAL
from .ui.intro import Ui_Intro
import datetime
import mozinfo
from PySide2.QtCore import SIGNAL, QDate, QStringListModel, Qt, Slot
from PySide2.QtWidgets import QApplication, QCompleter, QMessageBox, QWizard, QWizardPage
from mozregression.branches import get_branches
from mozregression.dates import to_datetime
from mozregression.errors import DateFormatError, LauncherNotRunnable
from mozregression.fetch_configs import REGISTRY, create_config
from mozregression.launchers import REGISTRY as LAUNCHER_REGISTRY
from .ui.build_selection import Ui_BuildSelectionPage
from .ui.intro import Ui_Intro
from .ui.profile import Ui_Profile
from .ui.single_build_selection import Ui_SingleBuildSelectionPage
from mozregression.fetch_configs import create_config, REGISTRY
from mozregression.launchers import REGISTRY as LAUNCHER_REGISTRY
from mozregression.errors import LauncherNotRunnable, DateFormatError
from mozregression.dates import to_datetime
from mozregression.branches import get_branches
def resolve_obj_name(obj, name):
names = name.split('.')
names = name.split(".")
while names:
obj = getattr(obj, names.pop(0))
return obj
@ -25,8 +27,8 @@ def resolve_obj_name(obj, name):
class WizardPage(QWizardPage):
UI_CLASS = None
TITLE = ''
SUBTITLE = ''
TITLE = ""
SUBTITLE = ""
FIELDS = {}
def __init__(self):
@ -52,27 +54,31 @@ class WizardPage(QWizardPage):
class IntroPage(WizardPage):
UI_CLASS = Ui_Intro
TITLE = "Basic configuration"
SUBTITLE = ("Please choose an application and other options to specify"
" what you want to test.")
FIELDS = {'application': 'app_combo', "repository": "repository",
'bits': 'bits_combo', "build_type": "build_type", "url": "url"}
SUBTITLE = "Please choose an application and other options to specify" " what you want to test."
FIELDS = {
"application": "app_combo",
"repository": "repository",
"bits": "bits_combo",
"build_type": "build_type",
"url": "url",
}
def __init__(self):
WizardPage.__init__(self)
self.fetch_config = None
self.app_model = QStringListModel(
REGISTRY.names(
lambda klass: not getattr(klass, 'disable_in_gui', None)))
REGISTRY.names(lambda klass: not getattr(klass, "disable_in_gui", None))
)
self.ui.app_combo.setModel(self.app_model)
if mozinfo.bits == 64:
if mozinfo.os == 'mac':
self.bits_model = QStringListModel(['64'])
if mozinfo.os == "mac":
self.bits_model = QStringListModel(["64"])
bits_index = 0
else:
self.bits_model = QStringListModel(['32', '64'])
self.bits_model = QStringListModel(["32", "64"])
bits_index = 1
elif mozinfo.bits == 32:
self.bits_model = QStringListModel(['32'])
self.bits_model = QStringListModel(["32"])
bits_index = 0
self.ui.bits_combo.setModel(self.bits_model)
self.ui.bits_combo.setCurrentIndex(bits_index)
@ -80,8 +86,7 @@ class IntroPage(WizardPage):
self.ui.app_combo.currentIndexChanged.connect(self._set_fetch_config)
self.ui.bits_combo.currentIndexChanged.connect(self._set_fetch_config)
self.ui.app_combo.setCurrentIndex(
self.ui.app_combo.findText("firefox"))
self.ui.app_combo.setCurrentIndex(self.ui.app_combo.findText("firefox"))
self.ui.repository.textChanged.connect(self._on_repo_changed)
@ -91,13 +96,12 @@ class IntroPage(WizardPage):
QApplication.instance().focusChanged.connect(self._on_focus_changed)
def _on_repo_changed(self, text):
enable_release = (not text or text == 'mozilla-central')
enable_release = not text or text == "mozilla-central"
build_select_page = self.wizard().page(2)
if type(build_select_page) == SingleBuildSelectionPage:
build_menus = [build_select_page.ui.build]
else:
build_menus = [build_select_page.ui.start,
build_select_page.ui.end]
build_menus = [build_select_page.ui.start, build_select_page.ui.end]
for menu in build_menus:
menu.ui.combo_helper.model().item(1).setEnabled(enable_release)
if menu.ui.combo_helper.currentIndex() == 1:
@ -112,11 +116,9 @@ class IntroPage(WizardPage):
app_name = str(self.ui.app_combo.currentText())
bits = int(self.ui.bits_combo.currentText())
self.fetch_config = create_config(app_name, mozinfo.os, bits,
mozinfo.processor)
self.fetch_config = create_config(app_name, mozinfo.os, bits, mozinfo.processor)
self.build_type_model = QStringListModel(
self.fetch_config.available_build_types())
self.build_type_model = QStringListModel(self.fetch_config.available_build_types())
self.ui.build_type.setModel(self.build_type_model)
if not self.fetch_config.available_bits():
@ -127,7 +129,7 @@ class IntroPage(WizardPage):
self.ui.label_4.show()
# URL doesn't make sense for Thunderbird
if app_name == 'thunderbird':
if app_name == "thunderbird":
self.ui.url.hide()
self.ui.url_label.hide()
else:
@ -141,41 +143,37 @@ class IntroPage(WizardPage):
launcher_class.check_is_runnable()
return True
except LauncherNotRunnable as exc:
QMessageBox.critical(
self,
"%s is not runnable" % app_name,
str(exc)
)
QMessageBox.critical(self, "%s is not runnable" % app_name, str(exc))
return False
class ProfilePage(WizardPage):
UI_CLASS = Ui_Profile
TITLE = "Profile selection"
SUBTITLE = ("Choose a specific profile. You can choose an existing profile"
", or let this blank to use a new one.")
FIELDS = {"profile": "profile_widget.line_edit",
"profile_persistence": "profile_persistence_combo"}
SUBTITLE = (
"Choose a specific profile. You can choose an existing profile"
", or let this blank to use a new one."
)
FIELDS = {
"profile": "profile_widget.line_edit",
"profile_persistence": "profile_persistence_combo",
}
def __init__(self):
WizardPage.__init__(self)
profile_persistence_options = ["clone",
"clone-first",
"reuse"]
self.profile_persistence_model = \
QStringListModel(profile_persistence_options)
self.ui.profile_persistence_combo.setModel(
self.profile_persistence_model)
profile_persistence_options = ["clone", "clone-first", "reuse"]
self.profile_persistence_model = QStringListModel(profile_persistence_options)
self.ui.profile_persistence_combo.setModel(self.profile_persistence_model)
self.ui.profile_persistence_combo.setCurrentIndex(0)
def set_options(self, options):
WizardPage.set_options(self, options)
# get the prefs
options['preferences'] = self.get_prefs()
options["preferences"] = self.get_prefs()
# get the addons
options['addons'] = self.get_addons()
options["addons"] = self.get_addons()
# get the profile-persistence
options['profile_persistence'] = self.get_profile_persistence()
options["profile_persistence"] = self.get_profile_persistence()
def get_prefs(self):
return self.ui.pref_widget.get_prefs()
@ -190,8 +188,8 @@ class ProfilePage(WizardPage):
class BuildSelectionPage(WizardPage):
UI_CLASS = Ui_BuildSelectionPage
TITLE = "Build selection"
SUBTITLE = ("Select the range to bisect.")
FIELDS = {'find_fix': 'find_fix'}
SUBTITLE = "Select the range to bisect."
FIELDS = {"find_fix": "find_fix"}
def __init__(self):
WizardPage.__init__(self)
@ -202,10 +200,10 @@ class BuildSelectionPage(WizardPage):
def set_options(self, options):
WizardPage.set_options(self, options)
options['good'] = self.get_start()
options['bad'] = self.get_end()
if options['find_fix']:
options['good'], options['bad'] = options['bad'], options['good']
options["good"] = self.get_start()
options["bad"] = self.get_end()
if options["find_fix"]:
options["good"], options["bad"] = options["bad"], options["good"]
@Slot()
def change_labels(self):
@ -239,15 +237,11 @@ class BuildSelectionPage(WizardPage):
if end_date <= current:
return True
else:
QMessageBox.critical(
self,
"Error",
"You can't define a date in the future.")
QMessageBox.critical(self, "Error", "You can't define a date in the future.")
else:
QMessageBox.critical(
self,
"Error",
"The first date must be earlier than the second one.")
self, "Error", "The first date must be earlier than the second one."
)
return False
@ -259,8 +253,7 @@ class Wizard(QWizard):
self.resize(800, 600)
# associate current text to comboboxes fields instead of current index
self.setDefaultProperty("QComboBox", "currentText",
SIGNAL('currentIndexChanged(QString)'))
self.setDefaultProperty("QComboBox", "currentText", SIGNAL("currentIndexChanged(QString)"))
for klass in class_pages:
self.addPage(klass())
@ -271,32 +264,33 @@ class Wizard(QWizard):
self.page(page_id).set_options(options)
fetch_config = self.page(self.pageIds()[0]).fetch_config
fetch_config.set_repo(options['repository'])
fetch_config.set_build_type(options['build_type'])
fetch_config.set_repo(options["repository"])
fetch_config.set_build_type(options["build_type"])
# create a profile if required
launcher_class = LAUNCHER_REGISTRY.get(fetch_config.app_name)
if options['profile_persistence'] in ('clone-first', 'reuse'):
options['profile'] = launcher_class.create_profile(
profile=options['profile'],
addons=options['addons'],
preferences=options['preferences'],
clone=options['profile_persistence'] == 'clone-first')
if options["profile_persistence"] in ("clone-first", "reuse"):
options["profile"] = launcher_class.create_profile(
profile=options["profile"],
addons=options["addons"],
preferences=options["preferences"],
clone=options["profile_persistence"] == "clone-first",
)
return fetch_config, options
class BisectionWizard(Wizard):
def __init__(self, parent=None):
Wizard.__init__(self, "Bisection wizard",
(IntroPage, ProfilePage, BuildSelectionPage),
parent=parent)
Wizard.__init__(
self, "Bisection wizard", (IntroPage, ProfilePage, BuildSelectionPage), parent=parent,
)
class SingleBuildSelectionPage(WizardPage):
UI_CLASS = Ui_SingleBuildSelectionPage
TITLE = "Build selection"
SUBTITLE = ("Select the build you want to run.")
SUBTITLE = "Select the build you want to run."
def __init__(self):
WizardPage.__init__(self)
@ -305,11 +299,14 @@ class SingleBuildSelectionPage(WizardPage):
def set_options(self, options):
WizardPage.set_options(self, options)
options['launch'] = self.ui.build.get_value()
options["launch"] = self.ui.build.get_value()
class SingleRunWizard(Wizard):
def __init__(self, parent=None):
Wizard.__init__(self, "Single run wizard",
(IntroPage, ProfilePage, SingleBuildSelectionPage),
parent=parent)
Wizard.__init__(
self,
"Single run wizard",
(IntroPage, ProfilePage, SingleBuildSelectionPage),
parent=parent,
)

Просмотреть файл

@ -1,7 +1,8 @@
from PySide2.QtWidgets import QApplication
from PySide2.QtCore import QEventLoop, QTimer
from contextlib import contextmanager
from PySide2.QtCore import QEventLoop, QTimer
from PySide2.QtWidgets import QApplication
APP = QApplication([]) # we need an application to create widgets
@ -15,9 +16,11 @@ def wait_signal(signal, timeout=1):
timed_out = []
if timeout is not None:
def quit_with_error():
timed_out.append(1)
loop.quit()
QTimer.singleShot(timeout * 1000, quit_with_error)
loop.exec_()
if timed_out:

Просмотреть файл

@ -1,9 +1,10 @@
import pytest
import tempfile
import mozfile
from PySide2.QtCore import Qt
import mozfile
import pytest
from mock import patch
from PySide2.QtCore import Qt
from mozregui.addons_editor import AddonsWidgetEditor
@ -27,7 +28,7 @@ def test_create_addons_editor(addons_editor):
@pytest.fixture
def addons_file(request):
# create a temp addons file
f = tempfile.NamedTemporaryFile(suffix='.xpi', dir='.', delete=False)
f = tempfile.NamedTemporaryFile(suffix=".xpi", dir=".", delete=False)
f.close()
request.addfinalizer(lambda: mozfile.remove(f.name))
return f.name
@ -37,26 +38,19 @@ def test_add_addon(qtbot, addons_editor, addons_file):
with patch("mozregui.addons_editor.QFileDialog") as dlg:
filePath = addons_file
dlg.getOpenFileNames.return_value = ([filePath], "addon file (*.xpi)")
qtbot.mouseClick(
addons_editor.ui.add_addon,
Qt.LeftButton
)
qtbot.mouseClick(addons_editor.ui.add_addon, Qt.LeftButton)
dlg.getOpenFileNames.assert_called_once_with(
addons_editor,
"Choose one or more addon files",
filter="addon file (*.xpi)",
addons_editor, "Choose one or more addon files", filter="addon file (*.xpi)",
)
# check addons
assert addons_editor.list_model.rowCount() == len(
[filePath])
assert addons_editor.list_model.rowCount() == len([filePath])
assert addons_editor.get_addons() == [filePath]
def test_remove_addon(qtbot, addons_editor, addons_file):
test_add_addon(qtbot, addons_editor, addons_file)
addons_editor.ui.list_view.setCurrentIndex(addons_editor.list_model.index
(0))
addons_editor.ui.list_view.setCurrentIndex(addons_editor.list_model.index(0))
assert addons_editor.ui.list_view.selectedIndexes()
qtbot.mouseClick(addons_editor.ui.remove_addon, Qt.LeftButton)

Просмотреть файл

@ -1,17 +1,17 @@
import unittest
import time
import tempfile
import shutil
import os
import shutil
import tempfile
import time
import unittest
from mock import Mock, patch
from . import wait_signal
from PySide2.QtCore import QObject, QThread, Signal, \
Slot
from PySide2.QtCore import QObject, QThread, Signal, Slot
from mozregui import build_runner
from mozregression.persist_limit import PersistLimit
from mozregression.fetch_configs import create_config
from mozregression.persist_limit import PersistLimit
from mozregui import build_runner
from . import wait_signal
def mock_session():
@ -29,7 +29,7 @@ def mock_response(response, data, wait=0):
rest = rest[chunk_size:]
yield chunk
response.headers = {'Content-length': str(len(data))}
response.headers = {"Content-length": str(len(data))}
response.iter_content = iter_content
@ -39,33 +39,29 @@ class TestGuiBuildDownloadManager(unittest.TestCase):
tmpdir = tempfile.mkdtemp()
tpersist = PersistLimit(10 * 1073741824)
self.addCleanup(shutil.rmtree, tmpdir)
self.dl_manager = \
build_runner.GuiBuildDownloadManager(tmpdir, tpersist)
self.dl_manager = build_runner.GuiBuildDownloadManager(tmpdir, tpersist)
self.dl_manager.session = self.session
self.signals = {}
for sig in ('download_progress', 'download_started',
'download_finished'):
for sig in ("download_progress", "download_started", "download_finished"):
self.signals[sig] = Mock()
getattr(self.dl_manager, sig).connect(self.signals[sig])
@patch(
'mozregui.build_runner.GuiBuildDownloadManager._extract_download_info')
@patch("mozregui.build_runner.GuiBuildDownloadManager._extract_download_info")
def test_focus_download(self, extract_info):
extract_info.return_value = ('http://foo', 'foo')
mock_response(self.session_response, b'this is some data' * 10000, 0.01)
extract_info.return_value = ("http://foo", "foo")
mock_response(self.session_response, b"this is some data" * 10000, 0.01)
build_info = Mock()
with wait_signal(self.dl_manager.download_finished):
self.dl_manager.focus_download(build_info)
# build_path is defined
self.assertEqual(build_info.build_file,
self.dl_manager.get_dest('foo'))
self.assertEqual(build_info.build_file, self.dl_manager.get_dest("foo"))
# signals have been emitted
self.assertEqual(self.signals['download_started'].call_count, 1)
self.assertEqual(self.signals['download_finished'].call_count, 1)
self.assertGreater(self.signals['download_progress'].call_count, 0)
self.assertEqual(self.signals["download_started"].call_count, 1)
self.assertEqual(self.signals["download_finished"].call_count, 1)
self.assertGreater(self.signals["download_progress"].call_count, 0)
# well, file has been downloaded finally
self.assertTrue(os.path.isfile(build_info.build_file))
@ -79,9 +75,9 @@ class TestGuiTestRunner(unittest.TestCase):
self.test_runner.evaluate_started.connect(self.evaluate_started)
self.test_runner.evaluate_finished.connect(self.evaluate_finished)
@patch('mozregui.build_runner.create_launcher')
@patch("mozregui.build_runner.create_launcher")
def test_basic(self, create_launcher):
launcher = Mock(get_app_info=lambda: 'app_info')
launcher = Mock(get_app_info=lambda: "app_info")
create_launcher.return_value = launcher
# nothing called yet
@ -96,13 +92,13 @@ class TestGuiTestRunner(unittest.TestCase):
# launcher is defined
self.assertEqual(self.test_runner.launcher, launcher)
self.test_runner.finish('g')
self.test_runner.finish("g")
# now evaluate_finished has been called
self.assertEqual(self.evaluate_started.call_count, 1)
self.assertEqual(self.evaluate_finished.call_count, 1)
# verdict is defined, launcher is None
self.assertEqual(self.test_runner.verdict, 'g')
self.assertEqual(self.test_runner.verdict, "g")
def test_abstract_build_runner(qtbot):
@ -125,23 +121,21 @@ def test_abstract_build_runner(qtbot):
worker_class = Worker
def init_worker(self, fetch_config, options):
build_runner.AbstractBuildRunner.init_worker(self, fetch_config,
options)
build_runner.AbstractBuildRunner.init_worker(self, fetch_config, options)
self.thread.finished.connect(self.thread_finished)
self.worker.call_started.connect(self.call_started)
return self.worker.my_slot
# instantiate the runner
runner = BuildRunner(Mock(persist='.'))
runner = BuildRunner(Mock(persist="."))
assert not runner.thread
with qtbot.waitSignal(runner.thread_finished, raising=True):
with qtbot.waitSignal(runner.call_started, raising=True):
runner.start(
create_config('firefox', 'linux', 64, 'x86_64'),
{'addons': (), 'profile': '/path/to/profile',
'profile_persistence': 'clone'},
create_config("firefox", "linux", 64, "x86_64"),
{"addons": (), "profile": "/path/to/profile", "profile_persistence": "clone"},
)
runner.stop(True)
@ -158,15 +152,17 @@ def test_runner_started_multiple_times():
worker_class = Worker
def init_worker(self, fetch_config, options):
build_runner.AbstractBuildRunner.init_worker(self, fetch_config,
options)
build_runner.AbstractBuildRunner.init_worker(self, fetch_config, options)
return lambda: 1
fetch_config = create_config('firefox', 'linux', 64, 'x86_64')
options = {'addons': (), 'profile': '/path/to/profile',
'profile_persistence': 'clone'}
fetch_config = create_config("firefox", "linux", 64, "x86_64")
options = {
"addons": (),
"profile": "/path/to/profile",
"profile_persistence": "clone",
}
runner = BuildRunner(Mock(persist='.'))
runner = BuildRunner(Mock(persist="."))
assert not runner.stopped
runner.start(fetch_config, options)
assert not runner.stopped

Просмотреть файл

@ -1,8 +1,8 @@
import pytest
from mozregression import __version__
from mozregui.main import MainWindow
from mozregui.check_release import CheckRelease, QLabel, QUrl
from mozregui.main import MainWindow
@pytest.yield_fixture
@ -15,23 +15,18 @@ def mainwindow(qtbot):
def test_check_release(qtbot, mocker, mainwindow):
retry_get = mocker.patch("mozregui.check_release.retry_get")
retry_get.return_value = mocker.Mock(
json=lambda *a: {
'tag_name': '0.0',
'html_url': 'url'
}
)
retry_get.return_value = mocker.Mock(json=lambda *a: {"tag_name": "0.0", "html_url": "url"})
status_bar = mainwindow.ui.status_bar
assert status_bar.findChild(QLabel, '') is None
assert status_bar.findChild(QLabel, "") is None
checker = CheckRelease(mainwindow)
with qtbot.waitSignal(checker.thread.finished, raising=True):
checker.check()
lbl = status_bar.findChild(QLabel, '')
lbl = status_bar.findChild(QLabel, "")
assert lbl
assert "There is a new release available!" in str(lbl.text())
assert '0.0' in str(lbl.text())
assert "0.0" in str(lbl.text())
# simulate click on the link
open_url = mocker.patch("mozregui.check_release.QDesktopServices.openUrl")
@ -44,16 +39,13 @@ def test_check_release(qtbot, mocker, mainwindow):
def test_check_release_no_update(qtbot, mocker, mainwindow):
retry_get = mocker.patch("mozregui.check_release.retry_get")
retry_get.return_value = mocker.Mock(
json=lambda *a: {
'tag_name': __version__,
'html_url': 'url'
}
json=lambda *a: {"tag_name": __version__, "html_url": "url"}
)
status_bar = mainwindow.ui.status_bar
assert status_bar.findChild(QLabel, '') is None
assert status_bar.findChild(QLabel, "") is None
checker = CheckRelease(mainwindow)
with qtbot.waitSignal(checker.thread.finished, raising=True):
checker.check()
assert status_bar.findChild(QLabel, '') is None
assert status_bar.findChild(QLabel, "") is None

Просмотреть файл

@ -1,8 +1,8 @@
import pytest
from PySide2.QtCore import Qt
from PySide2.QtWidgets import QApplication, QPushButton
from mozregui.crash_reporter import CrashReporter, CrashDialog
from mozregui.crash_reporter import CrashDialog, CrashReporter
class CrashDlgTest(CrashDialog):
@ -46,4 +46,4 @@ def test_report_exception(crash_reporter, qtbot, mocker):
dlg = CrashDlgTest.INSTANCE
qtbot.waitForWindowShown(dlg)
text = str(dlg.ui.information.toPlainText())
assert 'oh, no!' in text
assert "oh, no!" in text

Просмотреть файл

@ -1,9 +1,9 @@
import pytest
import os
import tempfile
from PySide2.QtWidgets import QDialog
import pytest
from mock import Mock
from PySide2.QtWidgets import QDialog
from mozregui import global_prefs
@ -16,7 +16,7 @@ def write_default_conf():
conf_file.close()
def write_conf(text):
with open(conf_file.name, 'w') as f:
with open(conf_file.name, "w") as f:
f.write(text)
yield write_conf
@ -26,10 +26,12 @@ def write_default_conf():
def test_change_prefs_dialog(write_default_conf, qtbot):
write_default_conf("""
write_default_conf(
"""
http-timeout = 32.1
persist-size-limit = 2.5
""")
"""
)
pref_dialog = global_prefs.ChangePrefsDialog()
qtbot.add_widget(pref_dialog)
@ -37,7 +39,7 @@ persist-size-limit = 2.5
qtbot.waitForWindowShown(pref_dialog)
# defaults are set
assert str(pref_dialog.ui.persist.line_edit.text()) == ''
assert str(pref_dialog.ui.persist.line_edit.text()) == ""
assert pref_dialog.ui.http_timeout.value() == 32.1
assert pref_dialog.ui.persist_size_limit.value() == 2.5
@ -48,15 +50,12 @@ persist-size-limit = 2.5
pref_dialog.save_prefs()
# check they have been registered
assert global_prefs.get_prefs().get('persist') == "/path/to"
assert global_prefs.get_prefs().get("persist") == "/path/to"
@pytest.mark.parametrize('dlg_result,saved', [
(QDialog.Accepted, True),
(QDialog.Rejected, False),
])
@pytest.mark.parametrize("dlg_result,saved", [(QDialog.Accepted, True), (QDialog.Rejected, False)])
def test_change_prefs_dialog_saves_prefs(dlg_result, saved, mocker):
Dlg = mocker.patch('mozregui.global_prefs.ChangePrefsDialog')
Dlg = mocker.patch("mozregui.global_prefs.ChangePrefsDialog")
dlg = Mock()
Dlg.return_value = dlg
dlg.exec_.return_value = dlg_result

Просмотреть файл

@ -1,6 +1,7 @@
import pytest
import time
import pytest
from mozregui import log_report
@ -17,42 +18,40 @@ def log_view(qtbot):
def test_log_report_report_log_line(log_view):
# view is first empty
assert str(log_view.toPlainText()) == ''
assert str(log_view.toPlainText()) == ""
assert log_view.blockCount() == 1 # 1 for an empty document
# send a log line
log_view.log_model({'message': 'my message', 'level': 'INFO',
'time': time.time()})
log_view.log_model({"message": "my message", "level": "INFO", "time": time.time()})
assert log_view.blockCount() == 2
assert 'INFO : my message' in str(log_view.toPlainText())
assert "INFO : my message" in str(log_view.toPlainText())
def test_log_report_report_no_more_than_1000_lines(log_view):
for i in range(1001):
log_view.log_model({'message': str(i), 'level': 'INFO',
'time': time.time()})
log_view.log_model({"message": str(i), "level": "INFO", "time": time.time()})
assert log_view.blockCount() == 1000
lines = str(log_view.toPlainText()).splitlines()
assert len(lines) == 999
assert 'INFO : 1000' in lines[-1]
assert 'INFO : 2' in lines[0] # 2 first lines were dropped
assert "INFO : 1000" in lines[-1]
assert "INFO : 2" in lines[0] # 2 first lines were dropped
def test_log_report_sets_correct_user_data(log_view):
"""Assumes that only the correct log levels are entered"""
# Inserts a log message for each log user level
for log_level in log_report.log_levels.keys():
log_view.log_model({'message': "%s message" % log_level,
'level': '%s' % log_level, 'time': time.time()})
log_view.log_model(
{"message": "%s message" % log_level, "level": "%s" % log_level, "time": time.time()}
)
# Checks each log level message to make sure the correct
# user data is entered
for current_block in log_view.text_blocks():
for log_level in log_report.log_levels.keys():
if log_level in current_block.text():
assert current_block.userData().log_lvl == \
log_report.log_levels[log_level]
assert current_block.userData().log_lvl == log_report.log_levels[log_level]
def test_log_report_filters_data_below_current_log_level(log_view):
@ -61,8 +60,9 @@ def test_log_report_filters_data_below_current_log_level(log_view):
current_log_level = log_view.log_lvl
# Inserts a log message for each log user level
for log_level in log_report.log_levels.keys():
log_view.log_model({'message': "%s message" % log_level,
'level': '%s' % log_level, 'time': time.time()})
log_view.log_model(
{"message": "%s message" % log_level, "level": "%s" % log_level, "time": time.time()}
)
# Check that log messages above the current log level are visible
# and log messages below the log level are invisible
for current_block in log_view.text_blocks():

Просмотреть файл

@ -1,10 +1,12 @@
import os
from mock import patch
from PySide2.QtCore import QTimer
from . import APP
from mozregui import main # noqa
from . import APP
@patch("mozregui.main.QApplication")
@patch("mozregui.main.CheckRelease")

Просмотреть файл

@ -1,11 +1,12 @@
import pytest
import tempfile
import mozfile
import os
import mozinfo
import tempfile
from PySide2.QtCore import Qt
import mozfile
import mozinfo
import pytest
from mock import patch
from PySide2.QtCore import Qt
from mozregui.pref_editor import PreferencesWidgetEditor
@ -54,7 +55,7 @@ def test_add_empty_pref_then_fill_it(qtbot, pref_editor):
# under TRAVIS and mac, the edition fail somewhere. not sure
# why, since on my ubuntu it works well even with Xvfb.
# TODO: look at this later
assert pref_editor.get_prefs() == [('hello', 'world')]
assert pref_editor.get_prefs() == [("hello", "world")]
def test_remove_pref(qtbot, pref_editor):
@ -76,7 +77,7 @@ def test_remove_pref(qtbot, pref_editor):
@pytest.fixture
def pref_file(request):
# create a temp file with prefs
with tempfile.NamedTemporaryFile(suffix='.json', delete=False) as f:
with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as f:
f.write(b'{ "browser.tabs.remote.autostart": false, "toto": 1 }')
request.addfinalizer(lambda: mozfile.remove(f.name))
return f.name
@ -84,20 +85,14 @@ def pref_file(request):
def test_add_prefs_using_file(qtbot, pref_editor, pref_file):
with patch("mozregui.pref_editor.QFileDialog") as dlg:
dlg.getOpenFileName.return_value = (pref_file, 'pref file (*.json *.ini)')
qtbot.mouseClick(
pref_editor.ui.add_prefs_from_file,
Qt.LeftButton
)
dlg.getOpenFileName.return_value = (pref_file, "pref file (*.json *.ini)")
qtbot.mouseClick(pref_editor.ui.add_prefs_from_file, Qt.LeftButton)
dlg.getOpenFileName.assert_called_once_with(
pref_editor,
"Choose a preference file",
filter="pref file (*.json *.ini)"
pref_editor, "Choose a preference file", filter="pref file (*.json *.ini)"
)
# check prefs
assert pref_editor.pref_model.rowCount() == 2
assert set(pref_editor.get_prefs()) == set([
("browser.tabs.remote.autostart", False),
("toto", 1)
])
assert set(pref_editor.get_prefs()) == set(
[("browser.tabs.remote.autostart", False), ("toto", 1)]
)

Просмотреть файл

@ -1,5 +1,5 @@
from PySide2.QtCore import Qt
from mock import Mock
from PySide2.QtCore import Qt
from mozregression.build_info import NightlyBuildInfo
from mozregui.report import ReportView
@ -24,30 +24,25 @@ def test_report_basic(qtbot):
assert index.isValid()
# simulate a build found
data = dict(build_type='nightly', build_date='date',
repo_name='mozilla-central')
build_infos = Mock(
spec=NightlyBuildInfo,
to_dict=lambda: data,
**data
)
bisection = Mock(build_range=[Mock(repo_name='mozilla-central')])
data = dict(build_type="nightly", build_date="date", repo_name="mozilla-central")
build_infos = Mock(spec=NightlyBuildInfo, to_dict=lambda: data, **data)
bisection = Mock(build_range=[Mock(repo_name="mozilla-central")])
bisection.handler.find_fix = False
bisection.handler.get_range.return_value = ('1', '2')
bisection.handler.get_range.return_value = ("1", "2")
view.model().step_build_found(bisection, build_infos)
# now we have two rows
assert view.model().rowCount() == 2
# data is updated
index2 = view.model().index(1, 0)
assert '[1 - 2]' in view.model().data(index)
assert 'mozilla-central' in view.model().data(index2)
assert "[1 - 2]" in view.model().data(index)
assert "mozilla-central" in view.model().data(index2)
# simulate a build evaluation
view.model().step_finished(None, 'g')
view.model().step_finished(None, "g")
# still two rows
assert view.model().rowCount() == 2
# data is updated
assert 'g' in view.model().data(index2)
assert "g" in view.model().data(index2)
# let's click on the index
view.scrollTo(index)

Просмотреть файл

@ -1,8 +1,8 @@
import pytest
from PySide2.QtCore import Qt
from PySide2.QtGui import QCloseEvent
from PySide2.QtWidgets import QMessageBox
from mozregression.build_range import BuildRange, FutureBuildInfo
from mozregui.skip_chooser import SkipDialog
@ -16,8 +16,7 @@ class DialogBuilder(object):
def _fetch(self):
return self.data
build_range = BuildRange(None, [FInfo(None, i)
for i in range(nb_builds)])
build_range = BuildRange(None, [FInfo(None, i) for i in range(nb_builds)])
dialog = SkipDialog(build_range)
dialog.exec_ = lambda: return_exec_code
self.qtbot.addWidget(dialog)
@ -72,8 +71,7 @@ def test_dbl_click_btn(qtbot, dialog_builder):
def test_close_event(mocker, dialog_builder, close):
dialog = dialog_builder.build(5)
warning = mocker.patch("PySide2.QtWidgets.QMessageBox.warning")
warning.return_value = (QMessageBox.Yes if close
else QMessageBox.No)
warning.return_value = QMessageBox.Yes if close else QMessageBox.No
evt = QCloseEvent()
dialog.closeEvent(evt)

Просмотреть файл

@ -1,8 +1,9 @@
import pytest
import datetime
from mozregui.utils import BuildSelection
import pytest
from mozregression.errors import DateFormatError
from mozregui.utils import BuildSelection
@pytest.fixture
@ -15,7 +16,7 @@ def build_selection(qtbot):
def test_date_widget_is_visible_by_default(build_selection):
assert build_selection.ui.combo_helper.currentText() == 'date'
assert build_selection.ui.combo_helper.currentText() == "date"
assert build_selection.ui.date.isVisible()
assert not build_selection.ui.buildid.isVisible()
assert not build_selection.ui.release.isVisible()
@ -23,18 +24,20 @@ def test_date_widget_is_visible_by_default(build_selection):
def test_switch_to_release_widget(build_selection, qtbot):
qtbot.keyClicks(build_selection.ui.combo_helper, "release")
assert build_selection.ui.combo_helper.currentText() == 'release'
assert build_selection.ui.combo_helper.currentText() == "release"
assert not build_selection.ui.date.isVisible()
assert not build_selection.ui.buildid.isVisible()
assert build_selection.ui.release.isVisible()
@pytest.mark.parametrize("widname,value,expected", [
("buildid", "20150102101112",
datetime.datetime(2015, 1, 2, 10, 11, 12)),
("release", "40", datetime.date(2015, 5, 11)),
("changeset", "abc123", "abc123"),
])
@pytest.mark.parametrize(
"widname,value,expected",
[
("buildid", "20150102101112", datetime.datetime(2015, 1, 2, 10, 11, 12)),
("release", "40", datetime.date(2015, 5, 11)),
("changeset", "abc123", "abc123"),
],
)
def test_get_value(build_selection, qtbot, widname, value, expected):
qtbot.keyClicks(build_selection.ui.combo_helper, widname)
qtbot.keyClicks(getattr(build_selection.ui, widname), value)
@ -51,8 +54,7 @@ def test_date_picker(build_selection, qtbot):
def test_get_invalid_buildid(build_selection, qtbot):
qtbot.keyClicks(build_selection.ui.combo_helper, "buildid")
qtbot.keyClicks(build_selection.ui.
stackedWidget.currentWidget(), "12345")
qtbot.keyClicks(build_selection.ui.stackedWidget.currentWidget(), "12345")
with pytest.raises(DateFormatError) as ctx:
build_selection.get_value()
assert 'build id' in str(ctx.value)
assert "build id" in str(ctx.value)

Просмотреть файл

@ -1,27 +1,33 @@
import pytest
from PySide2.QtCore import QDate
from mozregression.fetch_configs import CommonConfig
from mozregui.wizard import BisectionWizard, IntroPage, ProfilePage, \
BuildSelectionPage, SingleBuildSelectionPage, SingleRunWizard
from mozregui.wizard import (
BisectionWizard,
BuildSelectionPage,
IntroPage,
ProfilePage,
SingleBuildSelectionPage,
SingleRunWizard,
)
PAGES_BISECTION_WIZARD = (IntroPage, ProfilePage, BuildSelectionPage)
PAGES_SINGLE_RUN_WIZARD = (IntroPage, ProfilePage, SingleBuildSelectionPage)
@pytest.mark.parametrize('os, bits, wizard_class, pages', [
('linux', 64, BisectionWizard, PAGES_BISECTION_WIZARD,),
('win', 32, BisectionWizard, PAGES_BISECTION_WIZARD),
('mac', 64, BisectionWizard, PAGES_BISECTION_WIZARD),
('linux', 64, SingleRunWizard, PAGES_SINGLE_RUN_WIZARD),
('win', 32, SingleRunWizard, PAGES_SINGLE_RUN_WIZARD),
('mac', 64, SingleRunWizard, PAGES_SINGLE_RUN_WIZARD),
])
@pytest.mark.parametrize(
"os, bits, wizard_class, pages",
[
("linux", 64, BisectionWizard, PAGES_BISECTION_WIZARD,),
("win", 32, BisectionWizard, PAGES_BISECTION_WIZARD),
("mac", 64, BisectionWizard, PAGES_BISECTION_WIZARD),
("linux", 64, SingleRunWizard, PAGES_SINGLE_RUN_WIZARD),
("win", 32, SingleRunWizard, PAGES_SINGLE_RUN_WIZARD),
("mac", 64, SingleRunWizard, PAGES_SINGLE_RUN_WIZARD),
],
)
def test_wizard(mocker, qtbot, os, bits, wizard_class, pages):
mozinfo = mocker.patch('mozregui.wizard.mozinfo')
mozinfo = mocker.patch("mozregui.wizard.mozinfo")
mozinfo.os = os
mozinfo.bits = bits
@ -42,15 +48,15 @@ def test_wizard(mocker, qtbot, os, bits, wizard_class, pages):
now = QDate.currentDate()
assert isinstance(fetch_config, CommonConfig)
assert fetch_config.app_name == 'firefox' # this is the default
assert fetch_config.app_name == "firefox" # this is the default
assert fetch_config.os == os
assert fetch_config.bits == bits
assert fetch_config.build_type == 'shippable'
assert fetch_config.build_type == "shippable"
assert not fetch_config.repo
assert options['profile'] == ''
assert options["profile"] == ""
if isinstance(wizard, SingleRunWizard):
assert options['launch'] == now.addDays(-3).toPython()
assert options["launch"] == now.addDays(-3).toPython()
else:
assert options['good'] == now.addYears(-1).toPython()
assert options['bad'] == now.toPython()
assert options["good"] == now.addYears(-1).toPython()
assert options["bad"] == now.toPython()

Просмотреть файл

@ -3,7 +3,9 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
from __future__ import absolute_import
import re
from six.moves import range
@ -27,6 +29,7 @@ class ApproxPersistChooser(object):
2015-07-9
2015-07-11
"""
def __init__(self, one_every):
self.one_every = abs(one_every)

Просмотреть файл

@ -3,22 +3,28 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
from __future__ import absolute_import
import os
import math
import os
import threading
from abc import ABCMeta, abstractmethod
import six
from mozlog import get_proxy_logger
from mozregression.branches import find_branch_in_merge_commit, get_name
from mozregression.build_range import get_integration_range, get_nightly_range
from mozregression.dates import to_datetime
from mozregression.errors import LauncherError, MozRegressionError, \
GoodBadExpectationError, EmptyPushlogError
from mozregression.errors import (
EmptyPushlogError,
GoodBadExpectationError,
LauncherError,
MozRegressionError,
)
from mozregression.history import BisectionHistory
from mozregression.branches import find_branch_in_merge_commit, get_name
from mozregression.json_pushes import JsonPushes
from abc import ABCMeta, abstractmethod
import six
LOG = get_proxy_logger('Bisector')
LOG = get_proxy_logger("Bisector")
def compute_steps_left(steps):
@ -76,17 +82,15 @@ class BisectorHandler(six.with_metaclass(ABCMeta)):
# do not update repo if we can' find it now
# else we may override a previously defined one
self.found_repo = repo
self.good_revision, self.bad_revision = \
self._reverse_if_find_fix(self.build_range[0].changeset,
self.build_range[-1].changeset)
self.good_revision, self.bad_revision = self._reverse_if_find_fix(
self.build_range[0].changeset, self.build_range[-1].changeset
)
def get_pushlog_url(self):
first_rev, last_rev = self.get_range()
if first_rev == last_rev:
return "%s/pushloghtml?changeset=%s" % (
self.found_repo, first_rev)
return "%s/pushloghtml?fromchange=%s&tochange=%s" % (
self.found_repo, first_rev, last_rev)
return "%s/pushloghtml?changeset=%s" % (self.found_repo, first_rev)
return "%s/pushloghtml?fromchange=%s&tochange=%s" % (self.found_repo, first_rev, last_rev,)
def get_range(self):
return self._reverse_if_find_fix(self.good_revision, self.bad_revision)
@ -98,13 +102,17 @@ class BisectorHandler(six.with_metaclass(ABCMeta)):
"""
if full:
if good_date and bad_date:
good_date = ' (%s)' % good_date
bad_date = ' (%s)' % bad_date
words = self._reverse_if_find_fix('Last', 'First')
LOG.info("%s good revision: %s%s" % (
words[0], self.good_revision, good_date if good_date else ''))
LOG.info("%s bad revision: %s%s" % (
words[1], self.bad_revision, bad_date if bad_date else ''))
good_date = " (%s)" % good_date
bad_date = " (%s)" % bad_date
words = self._reverse_if_find_fix("Last", "First")
LOG.info(
"%s good revision: %s%s"
% (words[0], self.good_revision, good_date if good_date else "")
)
LOG.info(
"%s bad revision: %s%s"
% (words[1], self.bad_revision, bad_date if bad_date else "")
)
LOG.info("Pushlog:\n%s\n" % self.get_pushlog_url())
def build_good(self, mid, new_data):
@ -147,35 +155,33 @@ class NightlyHandler(BisectorHandler):
def initialize(self):
BisectorHandler.initialize(self)
# register dates
self.good_date, self.bad_date = \
self._reverse_if_find_fix(
self.build_range[0].build_date,
self.build_range[-1].build_date
)
self.good_date, self.bad_date = self._reverse_if_find_fix(
self.build_range[0].build_date, self.build_range[-1].build_date
)
def _print_progress(self, new_data):
next_good_date = new_data[0].build_date
next_bad_date = new_data[-1].build_date
next_days_range = abs((to_datetime(next_bad_date) -
to_datetime(next_good_date)).days)
LOG.info("Narrowed nightly regression window from"
" [%s, %s] (%d days) to [%s, %s] (%d days)"
" (~%d steps left)"
% (self.good_date,
self.bad_date,
abs((to_datetime(self.bad_date) -
to_datetime(self.good_date)).days),
next_good_date,
next_bad_date,
next_days_range,
compute_steps_left(next_days_range)))
next_days_range = abs((to_datetime(next_bad_date) - to_datetime(next_good_date)).days)
LOG.info(
"Narrowed nightly regression window from"
" [%s, %s] (%d days) to [%s, %s] (%d days)"
" (~%d steps left)"
% (
self.good_date,
self.bad_date,
abs((to_datetime(self.bad_date) - to_datetime(self.good_date)).days),
next_good_date,
next_bad_date,
next_days_range,
compute_steps_left(next_days_range),
)
)
def _print_date_range(self):
words = self._reverse_if_find_fix('Newest', 'Oldest')
LOG.info('%s known good nightly: %s' % (words[0],
self.good_date))
LOG.info('%s known bad nightly: %s' % (words[1],
self.bad_date))
words = self._reverse_if_find_fix("Newest", "Oldest")
LOG.info("%s known good nightly: %s" % (words[0], self.good_date))
LOG.info("%s known bad nightly: %s" % (words[1], self.bad_date))
def user_exit(self, mid):
self._print_date_range()
@ -190,14 +196,15 @@ class NightlyHandler(BisectorHandler):
if self.found_repo is None:
# this may happen if we are bisecting old builds without
# enough tests of the builds.
LOG.error("Sorry, but mozregression was unable to get"
" a repository - no pushlog url available.")
LOG.error(
"Sorry, but mozregression was unable to get"
" a repository - no pushlog url available."
)
# still we can print date range
if full:
self._print_date_range()
elif self.are_revisions_available():
BisectorHandler.print_range(self, self.good_date,
self.bad_date, full=full)
BisectorHandler.print_range(self, self.good_date, self.bad_date, full=full)
else:
if full:
self._print_date_range()
@ -209,31 +216,32 @@ class NightlyHandler(BisectorHandler):
return BisectorHandler.get_pushlog_url(self)
else:
start, end = self.get_date_range()
return ("%s/pushloghtml?startdate=%s&enddate=%s\n"
% (self.found_repo, start, end))
return "%s/pushloghtml?startdate=%s&enddate=%s\n" % (self.found_repo, start, end,)
class IntegrationHandler(BisectorHandler):
create_range = staticmethod(get_integration_range)
def _print_progress(self, new_data):
LOG.info("Narrowed integration regression window from [%s, %s]"
" (%d builds) to [%s, %s] (%d builds)"
" (~%d steps left)"
% (self.build_range[0].short_changeset,
self.build_range[-1].short_changeset,
len(self.build_range),
new_data[0].short_changeset,
new_data[-1].short_changeset,
len(new_data),
compute_steps_left(len(new_data))))
LOG.info(
"Narrowed integration regression window from [%s, %s]"
" (%d builds) to [%s, %s] (%d builds)"
" (~%d steps left)"
% (
self.build_range[0].short_changeset,
self.build_range[-1].short_changeset,
len(self.build_range),
new_data[0].short_changeset,
new_data[-1].short_changeset,
len(new_data),
compute_steps_left(len(new_data)),
)
)
def user_exit(self, mid):
words = self._reverse_if_find_fix('Newest', 'Oldest')
LOG.info('%s known good integration revision: %s'
% (words[0], self.good_revision))
LOG.info('%s known bad integration revision: %s'
% (words[1], self.bad_revision))
words = self._reverse_if_find_fix("Newest", "Oldest")
LOG.info("%s known good integration revision: %s" % (words[0], self.good_revision))
LOG.info("%s known bad integration revision: %s" % (words[1], self.bad_revision))
def _choose_integration_branch(self, changeset):
"""
@ -246,7 +254,7 @@ class IntegrationHandler(BisectorHandler):
jp = JsonPushes(k)
try:
push = jp.push(changeset, full='1')
push = jp.push(changeset, full="1")
landings[k] = push.timestamp
except EmptyPushlogError:
LOG.debug("Didn't find %s in %s" % (changeset, k))
@ -264,36 +272,42 @@ class IntegrationHandler(BisectorHandler):
# we have to check the commit of the most recent push
most_recent_push = self.build_range[1]
jp = JsonPushes(most_recent_push.repo_name)
push = jp.push(most_recent_push.changeset, full='1')
msg = push.changeset['desc']
push = jp.push(most_recent_push.changeset, full="1")
msg = push.changeset["desc"]
LOG.debug("Found commit message:\n%s\n" % msg)
branch = find_branch_in_merge_commit(msg, most_recent_push.repo_name)
if not (branch and len(push.changesets) >= 2):
# We did not find a branch, lets check the integration branches if we are bisecting m-c
LOG.debug("Did not find a branch, checking all integration branches")
if get_name(most_recent_push.repo_name) == 'mozilla-central' and \
len(push.changesets) >= 2:
if (
get_name(most_recent_push.repo_name) == "mozilla-central"
and len(push.changesets) >= 2
):
branch = self._choose_integration_branch(most_recent_push.changeset)
oldest = push.changesets[0]['node']
youngest = push.changesets[-1]['node']
LOG.info("************* Switching to %s by"
" process of elimination (no branch detected in"
" commit message)" % branch)
oldest = push.changesets[0]["node"]
youngest = push.changesets[-1]["node"]
LOG.info(
"************* Switching to %s by"
" process of elimination (no branch detected in"
" commit message)" % branch
)
else:
return
else:
# so, this is a merge. see how many changesets are in it, if it
# is just one, we have our answer
if len(push.changesets) == 2:
LOG.info("Merge commit has only two revisions (one of which "
"is the merge): we are done")
LOG.info(
"Merge commit has only two revisions (one of which "
"is the merge): we are done"
)
return
# Otherwise, we can find the oldest and youngest
# changesets, and the branch where the merge comes from.
oldest = push.changesets[0]['node']
oldest = push.changesets[0]["node"]
# exclude the merge commit
youngest = push.changesets[-2]['node']
youngest = push.changesets[-2]["node"]
LOG.info("************* Switching to %s" % branch)
# we can't use directly the oldest changeset because we
@ -307,12 +321,8 @@ class IntegrationHandler(BisectorHandler):
# changeset. This needs to be done on the right branch.
try:
jp2 = JsonPushes(branch)
raw = [int(p.push_id) for p in
jp2.pushes_within_changes(oldest, youngest)]
data = jp2.pushes(
startID=str(min(raw) - 2),
endID=str(max(raw)),
)
raw = [int(p.push_id) for p in jp2.pushes_within_changes(oldest, youngest)]
data = jp2.pushes(startID=str(min(raw) - 2), endID=str(max(raw)),)
older = data[0].changeset
youngest = data[-1].changeset
@ -325,12 +335,10 @@ class IntegrationHandler(BisectorHandler):
raise MozRegressionError(
"Unable to exploit the merge commit. Origin branch is {}, and"
" the commit message for {} was:\n{}".format(
most_recent_push.repo_name,
most_recent_push.short_changeset,
msg
most_recent_push.repo_name, most_recent_push.short_changeset, msg
)
)
LOG.debug('End merge handling')
LOG.debug("End merge handling")
return result
@ -340,14 +348,12 @@ class IndexPromise(object):
Provide a callable object that gives the next index when called.
"""
def __init__(self, index, callback=None, args=()):
self.thread = None
self.index = index
if callback:
self.thread = threading.Thread(
target=self._run,
args=(callback,) + args
)
self.thread = threading.Thread(target=self._run, args=(callback,) + args)
self.thread.start()
def _run(self, callback, *args):
@ -365,8 +371,15 @@ class Bisection(object):
FINISHED = 2
USER_EXIT = 3
def __init__(self, handler, build_range, download_manager, test_runner,
dl_in_background=True, approx_chooser=None):
def __init__(
self,
handler,
build_range,
download_manager,
test_runner,
dl_in_background=True,
approx_chooser=None,
):
self.handler = handler
self.build_range = build_range
self.download_manager = download_manager
@ -406,8 +419,7 @@ class Bisection(object):
is the dict of build infos for the build.
"""
build_infos = self.handler.build_range[mid_point]
return self._download_build(mid_point, build_infos,
allow_bg_download=allow_bg_download)
return self._download_build(mid_point, build_infos, allow_bg_download=allow_bg_download)
def _find_approx_build(self, mid_point, build_infos):
approx_index, persist_files = None, ()
@ -418,27 +430,24 @@ class Bisection(object):
# just act as usual, the downloader will take care of it.
if build_infos.persist_filename not in persist_files:
approx_index = self.approx_chooser.index(
self.build_range,
build_infos,
persist_files
self.build_range, build_infos, persist_files
)
if approx_index is not None:
# we found an approx build. First, stop possible background
# downloads, then update the mid point and build info.
if self.download_manager.background_dl_policy == 'cancel':
if self.download_manager.background_dl_policy == "cancel":
self.download_manager.cancel()
old_url = build_infos.build_url
mid_point = approx_index
build_infos = self.build_range[approx_index]
fname = self.download_manager.get_dest(
build_infos.persist_filename)
LOG.info("Using `%s` as an acceptable approximated"
" build file instead of downloading %s"
% (fname, old_url))
fname = self.download_manager.get_dest(build_infos.persist_filename)
LOG.info(
"Using `%s` as an acceptable approximated"
" build file instead of downloading %s" % (fname, old_url)
)
build_infos.build_file = fname
return (approx_index is not None, mid_point, build_infos,
persist_files)
return (approx_index is not None, mid_point, build_infos, persist_files)
def _download_build(self, mid_point, build_infos, allow_bg_download=True):
found, mid_point, build_infos, persist_files = self._find_approx_build(
@ -451,8 +460,7 @@ class Bisection(object):
callback = None
if self.dl_in_background and allow_bg_download:
callback = self._download_next_builds
return (IndexPromise(mid_point, callback, args=(persist_files,)),
build_infos)
return (IndexPromise(mid_point, callback, args=(persist_files,)), build_infos)
def _download_next_builds(self, mid_point, persist_files=()):
# start downloading the next builds.
@ -466,16 +474,19 @@ class Bisection(object):
m = r.mid_point()
if len(r) != 0:
# non-blocking download of the build
if self.approx_chooser and self.approx_chooser.index(
r, r[m], persist_files) is not None:
if (
self.approx_chooser
and self.approx_chooser.index(r, r[m], persist_files) is not None
):
pass # nothing to download, we have an approx build
else:
self.download_manager.download_in_background(r[m])
bdata = self.build_range[mid_point]
# download next left mid point
start_dl(self.build_range[mid_point:])
# download right next mid point
start_dl(self.build_range[:mid_point + 1])
start_dl(self.build_range[: mid_point + 1])
# since we called mid_point() on copy of self.build_range instance,
# the underlying cache may have changed and we need to find the new
# mid point.
@ -483,8 +494,7 @@ class Bisection(object):
return self.build_range.index(bdata)
def evaluate(self, build_infos):
verdict = self.test_runner.evaluate(build_infos,
allow_back=bool(self.history))
verdict = self.test_runner.evaluate(build_infos, allow_back=bool(self.history))
# old builds do not have metadata about the repo. But once
# the build is installed, we may have it
if self.handler.found_repo is None:
@ -496,8 +506,7 @@ class Bisection(object):
if self.handler.find_fix:
good, bad = bad, good
LOG.info("Testing good and bad builds to ensure that they are"
" really good and bad...")
LOG.info("Testing good and bad builds to ensure that they are" " really good and bad...")
self.download_manager.focus_download(good)
if self.dl_in_background:
self.download_manager.download_in_background(bad)
@ -507,28 +516,29 @@ class Bisection(object):
res = self.test_runner.evaluate(build_info)
if res == expected[0]:
return True
elif res == 's':
elif res == "s":
LOG.info("You can not skip this build.")
elif res == 'e':
elif res == "e":
return
elif res == 'r':
elif res == "r":
pass
else:
raise GoodBadExpectationError(
"Build was expected to be %s! The initial good/bad"
" range seems incorrect." % expected
)
if _evaluate(good, 'good'):
if _evaluate(good, "good"):
self.download_manager.focus_download(bad)
if self.dl_in_background:
# download next build (mid) in background
self.download_manager.download_in_background(
self.build_range[self.build_range.mid_point()]
)
return _evaluate(bad, 'bad')
return _evaluate(bad, "bad")
def handle_verdict(self, mid_point, verdict):
if verdict == 'g':
if verdict == "g":
# if build is good and we are looking for a regression, we
# have to split from
# [G, ?, ?, G, ?, B]
@ -538,9 +548,9 @@ class Bisection(object):
if not self.handler.find_fix:
self.build_range = self.build_range[mid_point:]
else:
self.build_range = self.build_range[:mid_point + 1]
self.build_range = self.build_range[: mid_point + 1]
self.handler.build_good(mid_point, self.build_range)
elif verdict == 'b':
elif verdict == "b":
# if build is bad and we are looking for a regression, we
# have to split from
# [G, ?, ?, B, ?, B]
@ -548,17 +558,17 @@ class Bisection(object):
# [G, ?, ?, B]
self.history.add(self.build_range, mid_point, verdict)
if not self.handler.find_fix:
self.build_range = self.build_range[:mid_point + 1]
self.build_range = self.build_range[: mid_point + 1]
else:
self.build_range = self.build_range[mid_point:]
self.handler.build_bad(mid_point, self.build_range)
elif verdict == 'r':
elif verdict == "r":
self.handler.build_retry(mid_point)
elif verdict == 's':
elif verdict == "s":
self.handler.build_skip(mid_point)
self.history.add(self.build_range, mid_point, verdict)
self.build_range = self.build_range.deleted(mid_point)
elif verdict == 'back':
elif verdict == "back":
self.build_range = self.history[-1].build_range
else:
# user exit
@ -572,8 +582,15 @@ class Bisector(object):
Handle the logic of the bisection process, and report events to a given
:class:`BisectorHandler`.
"""
def __init__(self, fetch_config, test_runner, download_manager,
dl_in_background=True, approx_chooser=None):
def __init__(
self,
fetch_config,
test_runner,
download_manager,
dl_in_background=True,
approx_chooser=None,
):
self.fetch_config = fetch_config
self.test_runner = test_runner
self.download_manager = download_manager
@ -583,10 +600,7 @@ class Bisector(object):
def bisect(self, handler, good, bad, **kwargs):
if handler.find_fix:
good, bad = bad, good
build_range = handler.create_range(self.fetch_config,
good,
bad,
**kwargs)
build_range = handler.create_range(self.fetch_config, good, bad, **kwargs)
return self._bisect(handler, build_range)
@ -595,10 +609,14 @@ class Bisector(object):
Starts a bisection for a :class:`mozregression.build_range.BuildData`.
"""
bisection = Bisection(handler, build_range, self.download_manager,
self.test_runner,
dl_in_background=self.dl_in_background,
approx_chooser=self.approx_chooser)
bisection = Bisection(
handler,
build_range,
self.download_manager,
self.test_runner,
dl_in_background=self.dl_in_background,
approx_chooser=self.approx_chooser,
)
previous_verdict = None
@ -609,41 +627,36 @@ class Bisector(object):
return result
if previous_verdict is None and handler.ensure_good_and_bad:
if bisection.ensure_good_and_bad():
LOG.info("Good and bad builds are correct. Let's"
" continue the bisection.")
LOG.info("Good and bad builds are correct. Let's" " continue the bisection.")
else:
return bisection.USER_EXIT
bisection.handler.print_range(full=False)
if previous_verdict == 'back':
if previous_verdict == "back":
index = bisection.history.pop(-1).index
allow_bg_download = True
if previous_verdict == 's':
if previous_verdict == "s":
# disallow background download since we are not sure of what
# to download next.
allow_bg_download = False
index = self.test_runner.index_to_try_after_skip(
bisection.build_range
)
index = self.test_runner.index_to_try_after_skip(bisection.build_range)
index_promise = None
build_info = bisection.build_range[index]
try:
if previous_verdict != 'r' and build_info:
if previous_verdict != "r" and build_info:
# if the last verdict was retry, do not download
# the build. Futhermore trying to download if we are
# in background download mode would stop the next builds
# from downloading.
index_promise, build_info = bisection.download_build(
index,
allow_bg_download=allow_bg_download
index, allow_bg_download=allow_bg_download
)
if not build_info:
LOG.info(
"Unable to find build info. Skipping this build...")
verdict = 's'
LOG.info("Unable to find build info. Skipping this build...")
verdict = "s"
else:
try:
verdict = bisection.evaluate(build_info)
@ -652,7 +665,7 @@ class Bisector(object):
# to run the tested app. We can just fallback
# to skip the build.
LOG.info("Error: %s. Skipping this build..." % exc)
verdict = 's'
verdict = "s"
finally:
# be sure to terminate the index_promise thread in all
# circumstances.

Просмотреть файл

@ -3,26 +3,27 @@ Access to mozilla branches information.
"""
from __future__ import absolute_import
import re
from collections import defaultdict
import six
from mozlog import get_proxy_logger
from mozregression.errors import MozRegressionError
import six
LOG = get_proxy_logger('Branches')
LOG = get_proxy_logger("Branches")
class Branches(object):
DEFAULT_REPO_URL = 'https://hg.mozilla.org/'
DEFAULT_REPO_URL = "https://hg.mozilla.org/"
def __init__(self):
self._branches = {}
self._aliases = {}
self._categories = defaultdict(list)
def set_branch(self, name, path, category='default'):
def set_branch(self, name, path, category="default"):
assert name not in self._branches, "branch %s already defined" % name
self._branches[name] = self.DEFAULT_REPO_URL + path
self._categories[category].append(name)
@ -41,8 +42,7 @@ class Branches(object):
try:
return self._branches[self.get_name(branch_name_or_alias)]
except KeyError:
raise MozRegressionError(
"No such branch '%s'." % branch_name_or_alias)
raise MozRegressionError("No such branch '%s'." % branch_name_or_alias)
def get_name(self, branch_name_or_alias):
return self._aliases.get(branch_name_or_alias) or branch_name_or_alias
@ -62,22 +62,21 @@ def create_branches():
# integration branches
for name in ("autoland", "mozilla-inbound"):
branches.set_branch(name, "integration/%s" % name,
category='integration')
branches.set_branch(name, "integration/%s" % name, category="integration")
# release branches
for name in ("comm-beta", "comm-release",
"mozilla-beta", "mozilla-release"):
branches.set_branch(name, "releases/%s" % name, category='releases')
for name in ("comm-beta", "comm-release", "mozilla-beta", "mozilla-release"):
branches.set_branch(name, "releases/%s" % name, category="releases")
branches.set_branch('try', 'try', category='try')
branches.set_branch("try", "try", category="try")
# aliases
for name, aliases in (
("mozilla-central", ("m-c", "central")),
("mozilla-inbound", ("m-i", "inbound", "mozilla inbound")),
("mozilla-beta", ("m-b", "beta")),
("mozilla-release", ("m-r", "release"))):
("mozilla-central", ("m-c", "central")),
("mozilla-inbound", ("m-i", "inbound", "mozilla inbound")),
("mozilla-beta", ("m-b", "beta")),
("mozilla-release", ("m-r", "release")),
):
for alias in aliases:
branches.set_alias(alias, name)
return branches

Просмотреть файл

@ -1,20 +1,22 @@
from __future__ import absolute_import
import re
from mozregression.json_pushes import JsonPushes
RE_BUG_ID = re.compile(r'bug\s+(\d+)', re.I)
RE_BUG_ID = re.compile(r"bug\s+(\d+)", re.I)
def find_bugids_in_push(branch, changeset):
jp = JsonPushes(branch)
push = jp.push(changeset, full='1')
push = jp.push(changeset, full="1")
branches = set()
for chset in push.changesets:
res = RE_BUG_ID.search(chset['desc'])
res = RE_BUG_ID.search(chset["desc"])
if res:
branches.add(res.group(1))
return [b for b in branches]
def bug_url(bugid):
return 'https://bugzilla.mozilla.org/show_bug.cgi?id=%s' % bugid
return "https://bugzilla.mozilla.org/show_bug.cgi?id=%s" % bugid

Просмотреть файл

@ -6,10 +6,11 @@
The BuildInfo classes, that are used to store information a build.
"""
from __future__ import absolute_import
import re
import datetime
from six.moves.urllib.parse import urlparse
import datetime
import re
from six.moves.urllib.parse import urlparse
FIELDS = []
@ -26,8 +27,18 @@ class BuildInfo(object):
a BuildInfo is built by calling
:meth:`mozregression.fetch_build_info.FetchBuildInfo.find_build_info`.
"""
def __init__(self, fetch_config, build_type, build_url, build_date,
changeset, repo_url, repo_name, task_id=None):
def __init__(
self,
fetch_config,
build_type,
build_url,
build_date,
changeset,
repo_url,
repo_name,
task_id=None,
):
self._fetch_config = fetch_config
self._build_type = build_type
self._build_url = build_url
@ -130,9 +141,9 @@ class BuildInfo(object):
This helps to build the pushlog url for old nightlies.
"""
if self._changeset is None:
self._changeset = app_info.get('application_changeset')
self._changeset = app_info.get("application_changeset")
if self._repo_url is None:
self._repo_url = app_info.get('application_repository')
self._repo_url = app_info.get("application_repository")
def persist_filename_for(self, data, regex=True):
"""
@ -146,34 +157,33 @@ class BuildInfo(object):
The pattern only allows the build name to be different, by using
the fetch_config.build_regex() value. For example, it can return:
'2015-01-11--mozilla-central--firefox.*linux-x86_64\.tar.bz2$'
'2015-01-11--mozilla-central--firefox.*linux-x86_64\\.tar.bz2$'
"""
if self.build_type == 'nightly':
if self.build_type == "nightly":
if isinstance(data, datetime.datetime):
prefix = data.strftime("%Y-%m-%d-%H-%M-%S")
else:
prefix = str(data)
persist_part = ''
persist_part = ""
else:
prefix = str(data[:12])
persist_part = self._fetch_config.integration_persist_part()
if persist_part:
persist_part = '-' + persist_part
full_prefix = '{}{}--{}--'.format(prefix, persist_part, self.repo_name)
persist_part = "-" + persist_part
full_prefix = "{}{}--{}--".format(prefix, persist_part, self.repo_name)
if regex:
full_prefix = re.escape(full_prefix)
appname = self._fetch_config.build_regex()
else:
appname = urlparse(self.build_url). \
path.replace('%2F', '/').split('/')[-1]
return '{}{}'.format(full_prefix, appname)
appname = urlparse(self.build_url).path.replace("%2F", "/").split("/")[-1]
return "{}{}".format(full_prefix, appname)
@property
def persist_filename(self):
"""
Compute and return the persist filename to use to store this build.
"""
if self.build_type == 'nightly':
if self.build_type == "nightly":
data = self.build_date
else:
data = self.changeset
@ -187,17 +197,29 @@ class BuildInfo(object):
class NightlyBuildInfo(BuildInfo):
def __init__(self, fetch_config, build_url, build_date, changeset,
repo_url):
BuildInfo.__init__(self, fetch_config, 'nightly', build_url,
build_date, changeset, repo_url,
fetch_config.get_nightly_repo(build_date))
def __init__(self, fetch_config, build_url, build_date, changeset, repo_url):
BuildInfo.__init__(
self,
fetch_config,
"nightly",
build_url,
build_date,
changeset,
repo_url,
fetch_config.get_nightly_repo(build_date),
)
class IntegrationBuildInfo(BuildInfo):
def __init__(self, fetch_config, build_url, build_date, changeset,
repo_url, task_id=None):
BuildInfo.__init__(self, fetch_config, 'integration', build_url,
build_date, changeset, repo_url,
fetch_config.integration_branch,
task_id=task_id)
def __init__(self, fetch_config, build_url, build_date, changeset, repo_url, task_id=None):
BuildInfo.__init__(
self,
fetch_config,
"integration",
build_url,
build_date,
changeset,
repo_url,
fetch_config.integration_branch,
task_id=task_id,
)

Просмотреть файл

@ -8,19 +8,18 @@ objects that are loaded on demand. A BuildRange is used for bisecting builds.
"""
from __future__ import absolute_import
import copy
import datetime
from threading import Thread
from mozlog import get_proxy_logger
from mozregression.dates import to_date, is_date_or_datetime, \
to_datetime
from mozregression.errors import BuildInfoNotFound
from mozregression.fetch_build_info import (IntegrationInfoFetcher,
NightlyInfoFetcher)
from mozlog import get_proxy_logger
from six.moves import range
from mozregression.dates import is_date_or_datetime, to_date, to_datetime
from mozregression.errors import BuildInfoNotFound
from mozregression.fetch_build_info import IntegrationInfoFetcher, NightlyInfoFetcher
LOG = get_proxy_logger("Bisector")
@ -76,6 +75,7 @@ class BuildRange(object):
- build_range[0:5] # slice operation, return a new build_range object
- build_range.deleted(5) # return a new build_range without item 5
"""
def __init__(self, build_info_fetcher, future_build_infos):
self.build_info_fetcher = build_info_fetcher
self._future_build_infos = future_build_infos
@ -90,35 +90,32 @@ class BuildRange(object):
def __getitem__(self, item):
if isinstance(item, slice):
if item.step not in (1, None):
raise ValueError('only step=1 supported')
raise ValueError("only step=1 supported")
new_range = copy.copy(self)
new_range._future_build_infos = self._future_build_infos[item.start:item.stop]
new_range._future_build_infos = self._future_build_infos[item.start : item.stop]
return new_range
return self._future_build_infos[item].build_info
def deleted(self, pos, count=1):
new_range = copy.copy(self)
new_range._future_build_infos = \
self._future_build_infos[:pos] + \
self._future_build_infos[pos + count:]
new_range._future_build_infos = (
self._future_build_infos[:pos] + self._future_build_infos[pos + count :]
)
return new_range
def filter_invalid_builds(self):
"""
Remove items that were unable to load BuildInfos.
"""
self._future_build_infos = \
[b for b in self._future_build_infos if b.is_valid()]
self._future_build_infos = [b for b in self._future_build_infos if b.is_valid()]
def _fetch(self, indexes):
indexes = set(indexes)
need_fetch = any(not self._future_build_infos[i].is_available()
for i in indexes)
need_fetch = any(not self._future_build_infos[i].is_available() for i in indexes)
if not need_fetch:
return
threads = [Thread(target=self.__getitem__, args=(i,))
for i in indexes]
threads = [Thread(target=self.__getitem__, args=(i,)) for i in indexes]
for thread in threads:
thread.daemon = True
thread.start()
@ -206,23 +203,25 @@ class BuildRange(object):
if self.get_future(0) != first:
new_first = search_last(range_before(first, expand))
if new_first:
LOG.info(
"Expanding lower limit of the range to %s" % new_first)
LOG.info("Expanding lower limit of the range to %s" % new_first)
self._future_build_infos.insert(0, new_first)
else:
LOG.critical("First build %s is missing, but mozregression"
" can't find a build before - so it is excluded,"
" but it could contain the regression!" % first)
LOG.critical(
"First build %s is missing, but mozregression"
" can't find a build before - so it is excluded,"
" but it could contain the regression!" % first
)
if self.get_future(-1) != last:
new_last = search_first(range_after(last, expand))
if new_last:
LOG.info(
"Expanding higher limit of the range to %s" % new_last)
LOG.info("Expanding higher limit of the range to %s" % new_last)
self._future_build_infos.append(new_last)
else:
LOG.critical("Last build %s is missing, but mozregression"
" can't find a build after - so it is excluded,"
" but it could contain the regression!" % last)
LOG.critical(
"Last build %s is missing, but mozregression"
" can't find a build after - so it is excluded,"
" but it could contain the regression!" % last
)
def index(self, build_info):
"""
@ -257,8 +256,7 @@ def _tc_build_range(future_tc, start_id, end_id):
def tc_range_after(future_tc, size):
"""Create a build range after a TCFutureBuildInfo"""
return _tc_build_range(future_tc, future_tc.data.push_id,
int(future_tc.data.push_id) + size)
return _tc_build_range(future_tc, future_tc.data.push_id, int(future_tc.data.push_id) + size)
def tc_range_before(future_tc, size):
@ -267,24 +265,23 @@ def tc_range_before(future_tc, size):
return _tc_build_range(future_tc, p_id - size, p_id)
def get_integration_range(fetch_config, start_rev, end_rev, time_limit=None,
expand=0, interrupt=None):
def get_integration_range(
fetch_config, start_rev, end_rev, time_limit=None, expand=0, interrupt=None
):
"""
Creates a BuildRange for integration builds.
"""
info_fetcher = IntegrationInfoFetcher(fetch_config)
jpushes = info_fetcher.jpushes
time_limit = time_limit or (datetime.datetime.now() +
datetime.timedelta(days=-365))
time_limit = time_limit or (datetime.datetime.now() + datetime.timedelta(days=-365))
def _check_date(obj):
if is_date_or_datetime(obj):
if to_datetime(obj) < time_limit:
LOG.info(
"TaskCluster only keeps builds for one year."
" Using %s instead of %s."
% (time_limit, obj)
" Using %s instead of %s." % (time_limit, obj)
)
obj = time_limit
return obj
@ -292,18 +289,17 @@ def get_integration_range(fetch_config, start_rev, end_rev, time_limit=None,
start_rev = _check_date(start_rev)
end_rev = _check_date(end_rev)
futures_builds = [TCFutureBuildInfo(info_fetcher, push)
for push in jpushes.pushes_within_changes(start_rev,
end_rev)]
futures_builds = [
TCFutureBuildInfo(info_fetcher, push)
for push in jpushes.pushes_within_changes(start_rev, end_rev)
]
br = BuildRange(info_fetcher, futures_builds)
if expand > 0:
br.check_expand(expand, tc_range_before, tc_range_after,
interrupt=interrupt)
br.check_expand(expand, tc_range_before, tc_range_after, interrupt=interrupt)
return br
def get_nightly_range(fetch_config, start_date, end_date, expand=0,
interrupt=None):
def get_nightly_range(fetch_config, start_date, end_date, expand=0, interrupt=None):
"""
Creates a BuildRange for nightlies.
"""
@ -312,12 +308,7 @@ def get_nightly_range(fetch_config, start_date, end_date, expand=0,
# build the build range using only dates
sd = to_date(start_date)
for i in range((to_date(end_date) - sd).days + 1):
futures_builds.append(
FutureBuildInfo(
info_fetcher,
sd + datetime.timedelta(days=i)
)
)
futures_builds.append(FutureBuildInfo(info_fetcher, sd + datetime.timedelta(days=i)))
# and now put back the real start and end dates
# in case they were datetime instances (coming from buildid)
futures_builds[0].data = start_date

Просмотреть файл

@ -11,7 +11,8 @@ class ClassRegistry(object):
:param attr_name: On each registered class, the unique name will be saved
under this class attribute name.
"""
def __init__(self, attr_name='name'):
def __init__(self, attr_name="name"):
self._classes = {}
self.attr_name = attr_name
@ -32,6 +33,7 @@ class ClassRegistry(object):
for key, value in six.iteritems(kwargs):
setattr(klass, key, value)
return klass
return wrapper
def get(self, name):

Просмотреть файл

@ -10,37 +10,39 @@ application.
:func:`cli` is intended to be the only public interface of this module.
"""
from __future__ import absolute_import
from __future__ import print_function
import os
import mozinfo
import datetime
import mozprofile
import re
from __future__ import absolute_import, print_function
from argparse import ArgumentParser, Action, SUPPRESS
import datetime
import os
import re
from argparse import SUPPRESS, Action, ArgumentParser
import mozinfo
import mozprofile
from mozlog.structuredlog import get_default_logger
from mozregression import __version__
from mozregression.branches import get_name
from mozregression.dates import to_datetime, parse_date, is_date_or_datetime
from mozregression.config import get_defaults, DEFAULT_CONF_FNAME, write_conf
from mozregression.config import DEFAULT_CONF_FNAME, get_defaults, write_conf
from mozregression.dates import is_date_or_datetime, parse_date, to_datetime
from mozregression.errors import DateFormatError, MozRegressionError, UnavailableRelease
from mozregression.fetch_configs import REGISTRY as FC_REGISTRY
from mozregression.fetch_configs import create_config
from mozregression.log import colorize, init_logger
from mozregression.releases import (
date_of_release,
formatted_valid_release_dates,
tag_of_beta,
tag_of_release,
)
from mozregression.tc_authenticate import tc_authenticate
from mozregression.fetch_configs import REGISTRY as FC_REGISTRY, create_config
from mozregression.errors import (MozRegressionError, DateFormatError,
UnavailableRelease)
from mozregression.releases import (formatted_valid_release_dates,
date_of_release, tag_of_release,
tag_of_beta)
from mozregression.log import init_logger, colorize
class _StopAction(Action):
def __init__(self, option_strings, dest=SUPPRESS, default=SUPPRESS,
help=None):
super(_StopAction, self).__init__(option_strings=option_strings,
dest=dest, default=default,
nargs=0, help=help)
def __init__(self, option_strings, dest=SUPPRESS, default=SUPPRESS, help=None):
super(_StopAction, self).__init__(
option_strings=option_strings, dest=dest, default=default, nargs=0, help=help,
)
def __call__(self, parser, namespace, values, option_string=None):
raise NotImplementedError
@ -80,219 +82,322 @@ def create_parser(defaults):
"""
Create the mozregression command line parser (ArgumentParser instance).
"""
usage = ("\n"
" %(prog)s [OPTIONS]"
" [--bad DATE|BUILDID|RELEASE|CHANGESET]"
" [--good DATE|BUILDID|RELEASE|CHANGESET]"
"\n"
" %(prog)s [OPTIONS] --launch DATE|BUILDID|RELEASE|CHANGESET"
"\n"
" %(prog)s --list-build-types"
"\n"
" %(prog)s --list-releases"
"\n"
" %(prog)s --write-conf")
usage = (
"\n"
" %(prog)s [OPTIONS]"
" [--bad DATE|BUILDID|RELEASE|CHANGESET]"
" [--good DATE|BUILDID|RELEASE|CHANGESET]"
"\n"
" %(prog)s [OPTIONS] --launch DATE|BUILDID|RELEASE|CHANGESET"
"\n"
" %(prog)s --list-build-types"
"\n"
" %(prog)s --list-releases"
"\n"
" %(prog)s --write-conf"
)
parser = ArgumentParser(usage=usage)
parser.add_argument("--version", action="version", version=__version__,
help=("print the mozregression version number and"
" exits."))
parser.add_argument(
"--version",
action="version",
version=__version__,
help=("print the mozregression version number and" " exits."),
)
parser.add_argument("-b", "--bad",
metavar="DATE|BUILDID|RELEASE|CHANGESET",
help=("first known bad build, default is today."
" It can be a date (YYYY-MM-DD), a build id,"
" a release number or a changeset. If it is"
" a changeset, the default branch will be the"
" integration branch of the application"
" (e.g. mozilla-inbound for firefox), else"
" the default release branch for the application"
" will be used as the default (e.g"
" mozilla-central for firefox)."))
parser.add_argument(
"-b",
"--bad",
metavar="DATE|BUILDID|RELEASE|CHANGESET",
help=(
"first known bad build, default is today."
" It can be a date (YYYY-MM-DD), a build id,"
" a release number or a changeset. If it is"
" a changeset, the default branch will be the"
" integration branch of the application"
" (e.g. mozilla-inbound for firefox), else"
" the default release branch for the application"
" will be used as the default (e.g"
" mozilla-central for firefox)."
),
)
parser.add_argument("-g", "--good",
metavar="DATE|BUILDID|RELEASE|CHANGESET",
help=("last known good build. Same possible values"
" as the --bad option."))
parser.add_argument(
"-g",
"--good",
metavar="DATE|BUILDID|RELEASE|CHANGESET",
help=("last known good build. Same possible values" " as the --bad option."),
)
parser.add_argument("--list-releases",
action=ListReleasesAction,
help="list all known releases and exit")
parser.add_argument(
"--list-releases", action=ListReleasesAction, help="list all known releases and exit",
)
parser.add_argument("-B", "--build-type",
default=defaults["build-type"],
help=("Build type to use, e.g. opt, debug. "
"See --list-build-types for available values. "
"Defaults to shippable for desktop Fx, opt for "
"everything else."))
parser.add_argument(
"-B",
"--build-type",
default=defaults["build-type"],
help=(
"Build type to use, e.g. opt, debug. "
"See --list-build-types for available values. "
"Defaults to shippable for desktop Fx, opt for "
"everything else."
),
)
parser.add_argument("--list-build-types", action=ListBuildTypesAction,
help="List available build types combinations.")
parser.add_argument(
"--list-build-types",
action=ListBuildTypesAction,
help="List available build types combinations.",
)
parser.add_argument("--find-fix", action="store_true",
help="Search for a bug fix instead of a regression.")
parser.add_argument(
"--find-fix", action="store_true", help="Search for a bug fix instead of a regression.",
)
parser.add_argument("-e", "--addon",
dest="addons",
action='append',
default=[],
metavar="PATH1",
help="addon to install; repeat for multiple addons.")
parser.add_argument(
"-e",
"--addon",
dest="addons",
action="append",
default=[],
metavar="PATH1",
help="addon to install; repeat for multiple addons.",
)
parser.add_argument("-p", "--profile",
default=defaults["profile"],
metavar="PATH",
help="profile to use with nightlies.")
parser.add_argument(
"-p",
"--profile",
default=defaults["profile"],
metavar="PATH",
help="profile to use with nightlies.",
)
parser.add_argument('--adb-profile-dir',
dest="adb_profile_dir",
default=defaults["adb-profile-dir"],
help=("Path to use on android devices for storing"
" the profile. Generally you should not need"
" to specify this, and an appropriate path"
" will be used. Specifying this to a value,"
" e.g. '/sdcard/tests' will forcibly try to create"
" the profile inside that folder."))
parser.add_argument(
"--adb-profile-dir",
dest="adb_profile_dir",
default=defaults["adb-profile-dir"],
help=(
"Path to use on android devices for storing"
" the profile. Generally you should not need"
" to specify this, and an appropriate path"
" will be used. Specifying this to a value,"
" e.g. '/sdcard/tests' will forcibly try to create"
" the profile inside that folder."
),
)
parser.add_argument('--profile-persistence',
choices=('clone', 'clone-first', 'reuse'),
default=defaults["profile-persistence"],
help=("Persistence of the used profile. Before"
" each tested build, a profile is used. If"
" the value of this option is 'clone', each"
" test uses a fresh clone. If the value is"
" 'clone-first', the profile is cloned once"
" then reused for all builds during the "
" bisection. If the value is 'reuse', the given"
" profile is directly used. Note that the"
" profile might be modified by mozregression."
" Defaults to %(default)s."))
parser.add_argument(
"--profile-persistence",
choices=("clone", "clone-first", "reuse"),
default=defaults["profile-persistence"],
help=(
"Persistence of the used profile. Before"
" each tested build, a profile is used. If"
" the value of this option is 'clone', each"
" test uses a fresh clone. If the value is"
" 'clone-first', the profile is cloned once"
" then reused for all builds during the "
" bisection. If the value is 'reuse', the given"
" profile is directly used. Note that the"
" profile might be modified by mozregression."
" Defaults to %(default)s."
),
)
parser.add_argument("-a", "--arg",
dest="cmdargs",
action='append',
default=[],
metavar="ARG1",
help=("a command-line argument to pass to the"
" application; repeat for multiple arguments."
" Use --arg='-option' to pass in options"
" starting with `-`."))
parser.add_argument(
"-a",
"--arg",
dest="cmdargs",
action="append",
default=[],
metavar="ARG1",
help=(
"a command-line argument to pass to the"
" application; repeat for multiple arguments."
" Use --arg='-option' to pass in options"
" starting with `-`."
),
)
parser.add_argument('--pref', nargs='*', dest='prefs',
help=(" A preference to set. Must be a key-value pair"
" separated by a ':'. Note that if your"
" preference is of type float, you should"
" pass it as a string, e.g.:"
" --pref \"layers.low-precision-opacity:'0.0'\""
))
parser.add_argument(
"--pref",
nargs="*",
dest="prefs",
help=(
" A preference to set. Must be a key-value pair"
" separated by a ':'. Note that if your"
" preference is of type float, you should"
" pass it as a string, e.g.:"
" --pref \"layers.low-precision-opacity:'0.0'\""
),
)
parser.add_argument('--preferences', nargs="*", dest='prefs_files',
help=("read preferences from a JSON or INI file. For"
" INI, use 'file.ini:section' to specify a"
" particular section."))
parser.add_argument(
"--preferences",
nargs="*",
dest="prefs_files",
help=(
"read preferences from a JSON or INI file. For"
" INI, use 'file.ini:section' to specify a"
" particular section."
),
)
parser.add_argument("-n", "--app",
choices=FC_REGISTRY.names(),
default=defaults["app"],
help="application name. Default: %(default)s.")
parser.add_argument(
"-n",
"--app",
choices=FC_REGISTRY.names(),
default=defaults["app"],
help="application name. Default: %(default)s.",
)
parser.add_argument("--repo",
metavar="[mozilla-inbound|autoland|mozilla-beta...]",
default=defaults["repo"],
help="repository name used for the bisection.")
parser.add_argument(
"--repo",
metavar="[mozilla-inbound|autoland|mozilla-beta...]",
default=defaults["repo"],
help="repository name used for the bisection.",
)
parser.add_argument("--bits",
choices=("32", "64"),
default=defaults["bits"],
help=("force 32 or 64 bit version (only applies to"
" x86_64 boxes). Default: %s bits."
% defaults["bits"] or mozinfo.bits))
parser.add_argument(
"--bits",
choices=("32", "64"),
default=defaults["bits"],
help=(
"force 32 or 64 bit version (only applies to"
" x86_64 boxes). Default: %s bits." % defaults["bits"]
or mozinfo.bits
),
)
parser.add_argument("-c", "--command",
help=("Test command to evaluate builds automatically."
" A return code of 0 will evaluate the build as"
" good, and any other value as bad."
" Variables like {binary} can be used, which"
" will be replaced with their value as retrieved"
" by the actual build."
))
parser.add_argument(
"-c",
"--command",
help=(
"Test command to evaluate builds automatically."
" A return code of 0 will evaluate the build as"
" good, and any other value as bad."
" Variables like {binary} can be used, which"
" will be replaced with their value as retrieved"
" by the actual build."
),
)
parser.add_argument("--persist",
default=defaults["persist"],
help=("the directory in which downloaded files are"
" to persist. Defaults to %(default)r."))
parser.add_argument(
"--persist",
default=defaults["persist"],
help=(
"the directory in which downloaded files are" " to persist. Defaults to %(default)r."
),
)
parser.add_argument('--persist-size-limit', type=float,
default=defaults['persist-size-limit'],
help=("Size limit for the persist directory in"
" gigabytes (GiB). When the limit is reached,"
" old builds are removed. 0 means no limit. Note"
" that at least 5 build files are kept,"
" regardless of this value."
" Defaults to %(default)s."))
parser.add_argument(
"--persist-size-limit",
type=float,
default=defaults["persist-size-limit"],
help=(
"Size limit for the persist directory in"
" gigabytes (GiB). When the limit is reached,"
" old builds are removed. 0 means no limit. Note"
" that at least 5 build files are kept,"
" regardless of this value."
" Defaults to %(default)s."
),
)
parser.add_argument('--http-timeout', type=float,
default=float(defaults['http-timeout']),
help=("Timeout in seconds to abort requests when there"
" is no activity from the server. Default to"
" %(default)s seconds - increase this if you"
" are under a really slow network."))
parser.add_argument(
"--http-timeout",
type=float,
default=float(defaults["http-timeout"]),
help=(
"Timeout in seconds to abort requests when there"
" is no activity from the server. Default to"
" %(default)s seconds - increase this if you"
" are under a really slow network."
),
)
parser.add_argument('--no-background-dl', action='store_false',
dest="background_dl",
default=(defaults['no-background-dl'].lower()
not in ('1', 'yes', 'true')),
help=("Do not download next builds in the background"
" while evaluating the current build."))
parser.add_argument(
"--no-background-dl",
action="store_false",
dest="background_dl",
default=(defaults["no-background-dl"].lower() not in ("1", "yes", "true")),
help=(
"Do not download next builds in the background" " while evaluating the current build."
),
)
parser.add_argument('--background-dl-policy', choices=('cancel', 'keep'),
default=defaults['background-dl-policy'],
help=('Policy to use for background downloads.'
' Possible values are "cancel" to cancel all'
' pending background downloads or "keep" to keep'
' downloading them when persist mode is enabled.'
' The default is %(default)s.'))
parser.add_argument(
"--background-dl-policy",
choices=("cancel", "keep"),
default=defaults["background-dl-policy"],
help=(
"Policy to use for background downloads."
' Possible values are "cancel" to cancel all'
' pending background downloads or "keep" to keep'
" downloading them when persist mode is enabled."
" The default is %(default)s."
),
)
parser.add_argument('--approx-policy', choices=('auto', 'none'),
default=defaults['approx-policy'],
help=("Policy to reuse approximate persistent builds"
" instead of downloading the accurate ones."
" When auto, mozregression will try its best to"
" reuse the files, meaning that for 7 days of"
" bisection range it will try to reuse a build"
" which date approximates the build to download"
" by one day (before or after). Use none to"
" disable this behavior."
" Defaults to %(default)s."))
parser.add_argument(
"--approx-policy",
choices=("auto", "none"),
default=defaults["approx-policy"],
help=(
"Policy to reuse approximate persistent builds"
" instead of downloading the accurate ones."
" When auto, mozregression will try its best to"
" reuse the files, meaning that for 7 days of"
" bisection range it will try to reuse a build"
" which date approximates the build to download"
" by one day (before or after). Use none to"
" disable this behavior."
" Defaults to %(default)s."
),
)
parser.add_argument('--launch',
metavar="DATE|BUILDID|RELEASE|CHANGESET",
help=("Launch only one specific build. Same possible"
" values as the --bad option."))
parser.add_argument(
"--launch",
metavar="DATE|BUILDID|RELEASE|CHANGESET",
help=("Launch only one specific build. Same possible" " values as the --bad option."),
)
parser.add_argument('-P', '--process-output', choices=('none', 'stdout'),
default=defaults['process-output'],
help=("Manage process output logging. Set to stdout by"
" default when the build type is not 'opt'."))
parser.add_argument(
"-P",
"--process-output",
choices=("none", "stdout"),
default=defaults["process-output"],
help=(
"Manage process output logging. Set to stdout by"
" default when the build type is not 'opt'."
),
)
parser.add_argument('-M', '--mode', choices=('classic', 'no-first-check'),
default=defaults['mode'],
help=("bisection mode. 'classic' will check for the"
" first good and bad builds to really be good"
" and bad, and 'no-first-check' won't. Defaults"
" to %(default)s."))
parser.add_argument(
"-M",
"--mode",
choices=("classic", "no-first-check"),
default=defaults["mode"],
help=(
"bisection mode. 'classic' will check for the"
" first good and bad builds to really be good"
" and bad, and 'no-first-check' won't. Defaults"
" to %(default)s."
),
)
parser.add_argument('--archive-base-url',
default=defaults['archive-base-url'],
help=("Base url used to find the archived builds."
" Defaults to %(default)s"))
parser.add_argument(
"--archive-base-url",
default=defaults["archive-base-url"],
help=("Base url used to find the archived builds." " Defaults to %(default)s"),
)
parser.add_argument('--write-config',
action=WriteConfigAction,
help="Helps to write the configuration file.")
parser.add_argument(
"--write-config", action=WriteConfigAction, help="Helps to write the configuration file.",
)
parser.add_argument('--debug', '-d', action='store_true',
help='Show the debug output.')
parser.add_argument("--debug", "-d", action="store_true", help="Show the debug output.")
return parser
@ -320,13 +425,13 @@ def preferences(prefs_files, prefs_args, logger):
for prefs_file in prefs_files:
prefs.add_file(prefs_file)
separator = ':'
separator = ":"
cli_prefs = []
if prefs_args:
for pref in prefs_args:
if separator not in pref:
if logger:
if '=' in pref:
if "=" in pref:
logger.warning('Pref %s has an "=", did you mean to use ":"?' % pref)
logger.info('Dropping pref %s for missing separator ":"' % pref)
continue
@ -343,14 +448,14 @@ def get_default_date_range(fetch_config):
Compute the default date range (first, last) to bisect.
"""
last_date = datetime.date.today()
if fetch_config.app_name == 'jsshell':
if fetch_config.app_name == "jsshell":
if fetch_config.os == "win" and fetch_config.bits == 64:
first_date = datetime.date(2014, 5, 27)
elif fetch_config.os == "linux" and "asan" in fetch_config.build_type:
first_date = datetime.date(2013, 9, 1)
else:
first_date = datetime.date(2012, 4, 18)
elif fetch_config.os == 'win' and fetch_config.bits == 64:
elif fetch_config.os == "win" and fetch_config.bits == 64:
# first firefox build date for win64 is 2010-05-28
first_date = datetime.date(2010, 5, 28)
else:
@ -377,17 +482,19 @@ class Configuration(object):
:attr fetch_config: the fetch_config instance, required to find
information about a build
"""
def __init__(self, options):
self.options = options
self.logger = init_logger(debug=options.debug)
# allow to filter process output based on the user option
if options.process_output is None:
# process_output not user defined
log_process_output = options.build_type != ''
log_process_output = options.build_type != ""
else:
log_process_output = options.process_output == 'stdout'
get_default_logger("process").component_filter = \
log_process_output = options.process_output == "stdout"
get_default_logger("process").component_filter = (
lambda data: data if log_process_output else None
)
# filter some mozversion log lines
re_ignore_mozversion_line = re.compile(
@ -395,7 +502,7 @@ class Configuration(object):
r"|application_id|application_display_name): .+"
)
get_default_logger("mozversion").component_filter = lambda data: (
None if re_ignore_mozversion_line.match(data['message']) else data
None if re_ignore_mozversion_line.match(data["message"]) else data
)
self.action = None
@ -410,28 +517,27 @@ class Configuration(object):
except DateFormatError:
try:
repo = self.options.repo
if (get_name(repo) == 'mozilla-release' or
(not repo and re.match(r'^\d+\.\d\.\d$', value))):
if get_name(repo) == "mozilla-release" or (
not repo and re.match(r"^\d+\.\d\.\d$", value)
):
new_value = tag_of_release(value)
if not repo:
self.logger.info("Assuming repo mozilla-release")
self.fetch_config.set_repo('mozilla-release')
self.logger.info("Using tag %s for release %s"
% (new_value, value))
self.fetch_config.set_repo("mozilla-release")
self.logger.info("Using tag %s for release %s" % (new_value, value))
value = new_value
elif (get_name(repo) == 'mozilla-beta' or
(not repo and re.match(r'^\d+\.0b\d+$', value))):
elif get_name(repo) == "mozilla-beta" or (
not repo and re.match(r"^\d+\.0b\d+$", value)
):
new_value = tag_of_beta(value)
if not repo:
self.logger.info("Assuming repo mozilla-beta")
self.fetch_config.set_repo('mozilla-beta')
self.logger.info("Using tag %s for release %s"
% (new_value, value))
self.fetch_config.set_repo("mozilla-beta")
self.logger.info("Using tag %s for release %s" % (new_value, value))
value = new_value
else:
new_value = parse_date(date_of_release(value))
self.logger.info("Using date %s for release %s"
% (new_value, value))
self.logger.info("Using date %s for release %s" % (new_value, value))
value = new_value
except UnavailableRelease:
self.logger.info("%s is not a release, assuming it's a hash..." % value)
@ -446,30 +552,28 @@ class Configuration(object):
user_defined_bits = options.bits is not None
options.bits = parse_bits(options.bits or mozinfo.bits)
fetch_config = create_config(options.app, mozinfo.os, options.bits,
mozinfo.processor)
fetch_config = create_config(options.app, mozinfo.os, options.bits, mozinfo.processor)
if options.build_type:
try:
fetch_config.set_build_type(options.build_type)
except MozRegressionError as msg:
self.logger.warning(
"%s (Defaulting to %r)" % (msg, fetch_config.build_type)
)
self.logger.warning("%s (Defaulting to %r)" % (msg, fetch_config.build_type))
self.fetch_config = fetch_config
fetch_config.set_repo(options.repo)
fetch_config.set_base_url(options.archive_base_url)
if not user_defined_bits and \
options.bits == 64 and \
mozinfo.os == 'win' and \
32 in fetch_config.available_bits():
if (
not user_defined_bits
and options.bits == 64
and mozinfo.os == "win"
and 32 in fetch_config.available_bits()
):
# inform users on windows that we are using 64 bit builds.
self.logger.info("bits option not specified, using 64-bit builds.")
if options.bits == 32 and mozinfo.os == 'mac':
self.logger.info("only 64-bit builds available for mac, using "
"64-bit builds")
if options.bits == 32 and mozinfo.os == "mac":
self.logger.info("only 64-bit builds available for mac, using " "64-bit builds")
if fetch_config.tk_needs_auth():
creds = tc_authenticate(self.logger)
@ -479,50 +583,52 @@ class Configuration(object):
if options.launch:
options.launch = self._convert_to_bisect_arg(options.launch)
self.action = "launch_integration"
if is_date_or_datetime(options.launch) and \
fetch_config.should_use_archive():
if is_date_or_datetime(options.launch) and fetch_config.should_use_archive():
self.action = "launch_nightlies"
else:
# define good/bad default values if required
default_good_date, default_bad_date = \
get_default_date_range(fetch_config)
default_good_date, default_bad_date = get_default_date_range(fetch_config)
if options.find_fix:
default_bad_date, default_good_date = \
default_good_date, default_bad_date
default_bad_date, default_good_date = (
default_good_date,
default_bad_date,
)
if not options.bad:
options.bad = default_bad_date
self.logger.info("No 'bad' option specified, using %s"
% options.bad)
self.logger.info("No 'bad' option specified, using %s" % options.bad)
else:
options.bad = self._convert_to_bisect_arg(options.bad)
if not options.good:
options.good = default_good_date
self.logger.info("No 'good' option specified, using %s"
% options.good)
self.logger.info("No 'good' option specified, using %s" % options.good)
else:
options.good = self._convert_to_bisect_arg(options.good)
self.action = "bisect_integration"
if is_date_or_datetime(options.good) and \
is_date_or_datetime(options.bad):
if not options.find_fix and \
to_datetime(options.good) > to_datetime(options.bad):
if is_date_or_datetime(options.good) and is_date_or_datetime(options.bad):
if not options.find_fix and to_datetime(options.good) > to_datetime(options.bad):
raise MozRegressionError(
("Good date %s is later than bad date %s."
" Maybe you wanted to use the --find-fix"
" flag?") % (options.good, options.bad))
elif options.find_fix and \
to_datetime(options.good) < to_datetime(options.bad):
(
"Good date %s is later than bad date %s."
" Maybe you wanted to use the --find-fix"
" flag?"
)
% (options.good, options.bad)
)
elif options.find_fix and to_datetime(options.good) < to_datetime(options.bad):
raise MozRegressionError(
("Bad date %s is later than good date %s."
" You should not use the --find-fix flag"
" in this case...") % (options.bad, options.good))
(
"Bad date %s is later than good date %s."
" You should not use the --find-fix flag"
" in this case..."
)
% (options.bad, options.good)
)
if fetch_config.should_use_archive():
self.action = "bisect_nightlies"
options.preferences = preferences(options.prefs_files, options.prefs, self.logger)
# convert GiB to bytes.
options.persist_size_limit = \
int(abs(float(options.persist_size_limit)) * 1073741824)
options.persist_size_limit = int(abs(float(options.persist_size_limit)) * 1073741824)
def cli(argv=None, conf_file=DEFAULT_CONF_FNAME, namespace=None):
@ -541,12 +647,16 @@ def cli(argv=None, conf_file=DEFAULT_CONF_FNAME, namespace=None):
# we don't set the cmdargs default to be that from the
# configuration file, because then any new arguments
# will be appended: https://bugs.python.org/issue16399
options.cmdargs = defaults['cmdargs']
options.cmdargs = defaults["cmdargs"]
if conf_file and not os.path.isfile(conf_file):
print('*' * 10)
print(colorize("You should use a config file. Please use the " +
'{sBRIGHT}--write-config{sRESET_ALL}' +
" command line flag to help you create one."))
print('*' * 10)
print("*" * 10)
print(
colorize(
"You should use a config file. Please use the "
+ "{sBRIGHT}--write-config{sRESET_ALL}"
+ " command line flag to help you create one."
)
)
print("*" * 10)
print()
return Configuration(options)

Просмотреть файл

@ -6,32 +6,28 @@
Reading and writing of the configuration file.
"""
from __future__ import absolute_import
from __future__ import print_function
import os
import mozinfo
from __future__ import absolute_import, print_function
from configobj import ConfigObj, ParseError
import os
from datetime import datetime
from mozregression.log import colorize
from mozregression.errors import MozRegressionError
import mozinfo
from configobj import ConfigObj, ParseError
from six.moves import input
from mozregression.errors import MozRegressionError
from mozregression.log import colorize
CONFIG_FILE_HELP_URL = (
"http://mozilla.github.io/mozregression/documentation/configuration.html"
)
CONFIG_FILE_HELP_URL = "http://mozilla.github.io/mozregression/documentation/configuration.html"
DEFAULT_CONF_FNAME = os.path.expanduser(
os.path.join("~", ".mozilla", "mozregression", "mozregression.cfg")
)
TC_CREDENTIALS_FNAME = os.path.expanduser(
os.path.join("~", ".mozilla", "mozregression",
"taskcluster-credentials.json")
os.path.join("~", ".mozilla", "mozregression", "taskcluster-credentials.json")
)
OLD_TC_ROOT_URL = "https://taskcluster.net"
TC_ROOT_URL = "https://firefox-ci-tc.services.mozilla.com"
TC_ROOT_URL_MIGRATION_FLAG_DATE = datetime.strptime('2019-11-09', '%Y-%M-%d')
TC_ROOT_URL_MIGRATION_FLAG_DATE = datetime.strptime("2019-11-09", "%Y-%M-%d")
ARCHIVE_BASE_URL = "https://archive.mozilla.org/pub"
# when a bisection range needs to be expanded, the following value is used to
# specify how many builds we try (if 20, we will try 20 before the lower limit,
@ -41,25 +37,25 @@ DEFAULT_EXPAND = 20
# default values when not defined in config file.
# Note that this is also the list of options that can be used in config file
DEFAULTS = {
'adb-profile-dir': None,
'app': 'firefox',
'approx-policy': 'auto',
'archive-base-url': ARCHIVE_BASE_URL,
'background-dl-policy': 'cancel',
'bits': None,
'build-type': '',
'cmdargs': [],
'http-timeout': 30.0,
'mode': 'classic',
'no-background-dl': '',
'persist': None,
'persist-size-limit': 0,
'process-output': None,
'profile': None,
'profile-persistence': 'clone',
'repo': None,
'taskcluster-accesstoken': None,
'taskcluster-clientid': None,
"adb-profile-dir": None,
"app": "firefox",
"approx-policy": "auto",
"archive-base-url": ARCHIVE_BASE_URL,
"background-dl-policy": "cancel",
"bits": None,
"build-type": "",
"cmdargs": [],
"http-timeout": 30.0,
"mode": "classic",
"no-background-dl": "",
"persist": None,
"persist-size-limit": 0,
"process-output": None,
"profile": None,
"profile-persistence": "clone",
"repo": None,
"taskcluster-accesstoken": None,
"taskcluster-clientid": None,
}
@ -71,23 +67,25 @@ def get_defaults(conf_path):
try:
config = ConfigObj(conf_path)
except ParseError as exc:
raise MozRegressionError(
"Error while reading the config file %s:\n %s" % (conf_path, exc)
)
raise MozRegressionError("Error while reading the config file %s:\n %s" % (conf_path, exc))
defaults.update(config)
return defaults
def _get_persist_dir(default):
print("You should configure a persist directory, where to put downloaded"
" build files to reuse them in future bisections.")
print("I recommend using %s. Leave blank to use that default. If you"
" really don't want a persist dir type NONE, else you can"
" just define a path that you would like to use." % default)
print(
"You should configure a persist directory, where to put downloaded"
" build files to reuse them in future bisections."
)
print(
"I recommend using %s. Leave blank to use that default. If you"
" really don't want a persist dir type NONE, else you can"
" just define a path that you would like to use." % default
)
value = input("persist: ")
if value == "NONE":
return ''
return ""
elif value:
persist_dir = os.path.realpath(value)
else:
@ -100,11 +98,13 @@ def _get_persist_dir(default):
def _get_persist_size_limit(default):
print("You should choose a size limit for the persist dir. I recommend you"
" to use %s GiB, so leave it blank to use that default. Else you"
" can type NONE to not limit the persist dir, or any number you like"
" (a GiB value, so type 0.5 to allow ~500 MiB)." % default)
value = input('persist-size-limit: ')
print(
"You should choose a size limit for the persist dir. I recommend you"
" to use %s GiB, so leave it blank to use that default. Else you"
" can type NONE to not limit the persist dir, or any number you like"
" (a GiB value, so type 0.5 to allow ~500 MiB)." % default
)
value = input("persist-size-limit: ")
if value == "NONE":
return 0.0
elif value:
@ -113,12 +113,14 @@ def _get_persist_size_limit(default):
def _get_bits(default):
print("You are using a 64-bit system, so mozregression will by default"
" use the 64-bit build files. If you want to change that to"
" 32-bit by default, type 32 here.")
print(
"You are using a 64-bit system, so mozregression will by default"
" use the 64-bit build files. If you want to change that to"
" 32-bit by default, type 32 here."
)
while 1:
value = input('bits: ')
if value in ('', '32', '64'):
value = input("bits: ")
if value in ("", "32", "64"):
break
if not value:
return default
@ -156,22 +158,23 @@ def write_conf(conf_path):
else:
value = default
else:
print('%s already defined.' % optname)
print("%s already defined." % optname)
value = config[optname]
name = colorize("{fGREEN}%s{sRESET_ALL}" % optname)
print("%s: %s" % (name, value))
_set_option('persist', _get_persist_dir,
os.path.join(conf_dir, "persist"))
_set_option("persist", _get_persist_dir, os.path.join(conf_dir, "persist"))
_set_option('persist-size-limit', _get_persist_size_limit, 20.0)
_set_option("persist-size-limit", _get_persist_size_limit, 20.0)
if mozinfo.os != 'mac' and mozinfo.bits == 64:
_set_option('bits', _get_bits, 64)
if mozinfo.os != "mac" and mozinfo.bits == 64:
_set_option("bits", _get_bits, 64)
config.write()
print()
print(colorize('Config file {sBRIGHT}%s{sRESET_ALL} written.' % conf_path))
print("Note you can edit it manually, and there are other options you can"
" configure. See %s." % CONFIG_FILE_HELP_URL)
print(colorize("Config file {sBRIGHT}%s{sRESET_ALL} written." % conf_path))
print(
"Note you can edit it manually, and there are other options you can"
" configure. See %s." % CONFIG_FILE_HELP_URL
)

Просмотреть файл

@ -3,9 +3,10 @@ Date utilities functions.
"""
from __future__ import absolute_import
import re
import datetime
import calendar
import datetime
import re
from mozregression.errors import DateFormatError
@ -20,13 +21,11 @@ def parse_date(date_string):
return datetime.datetime.strptime(date_string, "%Y%m%d%H%M%S")
except ValueError:
raise DateFormatError(date_string, "Not a valid build id: `%s`")
regex = re.compile(r'(\d{4})\-(\d{1,2})\-(\d{1,2})')
regex = re.compile(r"(\d{4})\-(\d{1,2})\-(\d{1,2})")
matched = regex.match(date_string)
if not matched:
raise DateFormatError(date_string)
return datetime.date(int(matched.group(1)),
int(matched.group(2)),
int(matched.group(3)))
return datetime.date(int(matched.group(1)), int(matched.group(2)), int(matched.group(3)))
def to_datetime(date):

Просмотреть файл

@ -1,19 +1,19 @@
from __future__ import absolute_import
from __future__ import print_function
import tempfile
import threading
import requests
from __future__ import absolute_import, print_function
import os
import sys
import mozfile
import tempfile
import threading
from contextlib import closing
import mozfile
import requests
import six
from mozlog import get_proxy_logger
from mozregression.persist_limit import PersistLimit
import six
LOG = get_proxy_logger('Download')
LOG = get_proxy_logger("Download")
class DownloadInterrupt(Exception):
@ -45,11 +45,18 @@ class Download(object):
:param progress: A callable to report the progress (default to None).
see :meth:`set_progress`.
"""
def __init__(self, url, dest, finished_callback=None,
chunk_size=16 * 1024, session=requests, progress=None):
def __init__(
self,
url,
dest,
finished_callback=None,
chunk_size=16 * 1024,
session=requests,
progress=None,
):
self.thread = threading.Thread(
target=self._download,
args=(url, dest, finished_callback, chunk_size, session)
target=self._download, args=(url, dest, finished_callback, chunk_size, session),
)
self._lock = threading.Lock()
self.__url = url
@ -156,16 +163,14 @@ class Download(object):
bytes_so_far = 0
try:
with closing(session.get(url, stream=True)) as response:
total_size = int(response.headers['Content-length'].strip())
total_size = int(response.headers["Content-length"].strip())
self._update_progress(bytes_so_far, total_size)
# we use NamedTemporaryFile as raw open() call was causing
# issues on windows - see:
# https://bugzilla.mozilla.org/show_bug.cgi?id=1185756
with tempfile.NamedTemporaryFile(
delete=False,
mode='wb',
suffix='.tmp',
dir=os.path.dirname(dest)) as temp:
delete=False, mode="wb", suffix=".tmp", dir=os.path.dirname(dest)
) as temp:
for chunk in response.iter_content(chunk_size):
if self.is_canceled():
break
@ -225,6 +230,7 @@ class DownloadManager(object):
limiting the size of the download dir. Defaults
to None, meaning no limit.
"""
def __init__(self, destdir, session=requests, persist_limit=None):
self.destdir = destdir
self.session = session
@ -283,10 +289,13 @@ class DownloadManager(object):
# else create the download (will be automatically removed of
# the list on completion) start it, and returns that.
with self._lock:
download = Download(url, dest,
session=self.session,
finished_callback=self._download_finished,
progress=progress)
download = Download(
url,
dest,
session=self.session,
finished_callback=self._download_finished,
progress=progress,
)
self._downloads[dest] = download
download.start()
self._download_started(download)
@ -313,13 +322,13 @@ class BuildDownloadManager(DownloadManager):
"""
A DownloadManager specialized to download builds.
"""
def __init__(self, destdir, session=requests,
background_dl_policy='cancel',
persist_limit=None):
DownloadManager.__init__(self, destdir, session=session,
persist_limit=persist_limit)
def __init__(
self, destdir, session=requests, background_dl_policy="cancel", persist_limit=None,
):
DownloadManager.__init__(self, destdir, session=session, persist_limit=persist_limit)
self._downloads_bg = set()
assert background_dl_policy in ('cancel', 'keep')
assert background_dl_policy in ("cancel", "keep")
self.background_dl_policy = background_dl_policy
def _extract_download_info(self, build_info):
@ -360,7 +369,7 @@ class BuildDownloadManager(DownloadManager):
build_info.build_file = dest
# first, stop all downloads in background (except the one for this
# build if any)
if self.background_dl_policy == 'cancel':
if self.background_dl_policy == "cancel":
self.cancel(cancel_if=lambda dl: dest != dl.get_dest())
dl = self.download(build_url, fname, progress=download_progress)
@ -369,7 +378,7 @@ class BuildDownloadManager(DownloadManager):
try:
dl.wait()
finally:
print('') # a new line after download_progress calls
print("") # a new line after download_progress calls
else:
msg = "Using local file: %s" % dest

Просмотреть файл

@ -15,16 +15,16 @@ class WinTooOldBuildError(MozRegressionError):
"""
Raised when a windows build is too old.
"""
def __init__(self):
MozRegressionError.__init__(self,
"Can't run Windows builds before"
" 2010-03-18")
MozRegressionError.__init__(self, "Can't run Windows builds before" " 2010-03-18")
class DateFormatError(MozRegressionError):
"""
Raised when a date can not be parsed from a string.
"""
def __init__(self, date_string, format="Incorrect date format: `%s`"):
MozRegressionError.__init__(self, format % date_string)
@ -46,10 +46,11 @@ class UnavailableRelease(MozRegressionError):
"""
Raised when firefox release is not available.
"""
def __init__(self, release):
MozRegressionError.__init__(self,
"Unable to find a matching date for"
" release %s" % release)
MozRegressionError.__init__(
self, "Unable to find a matching date for" " release %s" % release
)
class LauncherError(MozRegressionError):

Просмотреть файл

@ -7,27 +7,28 @@ The public API is composed of two classes, :class:`NightlyInfoFetcher` and
"""
from __future__ import absolute_import
import os
import re
import taskcluster
from datetime import datetime
from taskcluster.exceptions import TaskclusterFailure
from mozlog import get_proxy_logger
from threading import Thread, Lock
from requests import HTTPError
from threading import Lock, Thread
from mozregression.config import (OLD_TC_ROOT_URL, TC_ROOT_URL,
TC_ROOT_URL_MIGRATION_FLAG_DATE)
from mozregression.network import url_links, retry_get
from mozregression.errors import BuildInfoNotFound, MozRegressionError
from mozregression.build_info import NightlyBuildInfo, IntegrationBuildInfo
from mozregression.json_pushes import JsonPushes, Push
LOG = get_proxy_logger(__name__)
# Fix intermittent bug due to strptime first call not being thread safe
# see https://bugzilla.mozilla.org/show_bug.cgi?id=1200270
# and http://bugs.python.org/issue7980
import _strptime # noqa
import taskcluster
from mozlog import get_proxy_logger
from requests import HTTPError
from taskcluster.exceptions import TaskclusterFailure
from mozregression.build_info import IntegrationBuildInfo, NightlyBuildInfo
from mozregression.config import OLD_TC_ROOT_URL, TC_ROOT_URL, TC_ROOT_URL_MIGRATION_FLAG_DATE
from mozregression.errors import BuildInfoNotFound, MozRegressionError
from mozregression.json_pushes import JsonPushes, Push
from mozregression.network import retry_get, url_links
LOG = get_proxy_logger(__name__)
class InfoFetcher(object):
@ -37,10 +38,8 @@ class InfoFetcher(object):
self.build_info_regex = re.compile(fetch_config.build_info_regex())
def _update_build_info_from_txt(self, build_info):
if 'build_txt_url' in build_info:
build_info.update(
self._fetch_txt_info(build_info['build_txt_url'])
)
if "build_txt_url" in build_info:
build_info.update(self._fetch_txt_info(build_info["build_txt_url"]))
def _fetch_txt_info(self, url):
"""
@ -52,18 +51,18 @@ class InfoFetcher(object):
data = {}
response = retry_get(url)
for line in response.text.splitlines():
if '/rev/' in line:
repository, changeset = line.split('/rev/')
data['repository'] = repository
data['changeset'] = changeset
if "/rev/" in line:
repository, changeset = line.split("/rev/")
data["repository"] = repository
data["changeset"] = changeset
break
if not data:
# the txt file could be in an old format:
# DATE CHANGESET
# we can try to extract that to get the changeset at least.
matched = re.match(r'^\d+ (\w+)$', response.text.strip())
matched = re.match(r"^\d+ (\w+)$", response.text.strip())
if matched:
data['changeset'] = matched.group(1)
data["changeset"] = matched.group(1)
return data
def find_build_info(self, changeset_or_date, fetch_txt_info=True):
@ -115,59 +114,59 @@ class IntegrationInfoFetcher(InfoFetcher):
task_id = None
status = None
for tc_root_url in possible_tc_root_urls:
LOG.debug('using taskcluster root url %s' % tc_root_url)
LOG.debug("using taskcluster root url %s" % tc_root_url)
options = self.fetch_config.tk_options(tc_root_url)
tc_index = taskcluster.Index(options)
tc_queue = taskcluster.Queue(options)
tk_routes = self.fetch_config.tk_routes(push)
stored_failure = None
for tk_route in tk_routes:
LOG.debug('using taskcluster route %r' % tk_route)
LOG.debug("using taskcluster route %r" % tk_route)
try:
task_id = tc_index.findTask(tk_route)['taskId']
task_id = tc_index.findTask(tk_route)["taskId"]
except TaskclusterFailure as ex:
LOG.debug('nothing found via route %r' % tk_route)
LOG.debug("nothing found via route %r" % tk_route)
stored_failure = ex
continue
if task_id:
status = tc_queue.status(task_id)['status']
status = tc_queue.status(task_id)["status"]
break
if status:
break
if not task_id:
raise stored_failure
except TaskclusterFailure:
raise BuildInfoNotFound("Unable to find build info using the"
" taskcluster route %r" %
self.fetch_config.tk_route(push))
raise BuildInfoNotFound(
"Unable to find build info using the"
" taskcluster route %r" % self.fetch_config.tk_route(push)
)
# find a completed run for that task
run_id, build_date = None, None
for run in reversed(status['runs']):
if run['state'] == 'completed':
run_id = run['runId']
build_date = datetime.strptime(run["resolved"],
'%Y-%m-%dT%H:%M:%S.%fZ')
for run in reversed(status["runs"]):
if run["state"] == "completed":
run_id = run["runId"]
build_date = datetime.strptime(run["resolved"], "%Y-%m-%dT%H:%M:%S.%fZ")
break
if run_id is None:
raise BuildInfoNotFound("Unable to find completed runs for task %s"
% task_id)
artifacts = tc_queue.listArtifacts(task_id, run_id)['artifacts']
raise BuildInfoNotFound("Unable to find completed runs for task %s" % task_id)
artifacts = tc_queue.listArtifacts(task_id, run_id)["artifacts"]
# look over the artifacts of that run
build_url = None
for a in artifacts:
name = os.path.basename(a['name'])
name = os.path.basename(a["name"])
if self.build_regex.search(name):
meth = tc_queue.buildUrl
if self.fetch_config.tk_needs_auth():
meth = tc_queue.buildSignedUrl
build_url = meth('getArtifact', task_id, run_id, a['name'])
build_url = meth("getArtifact", task_id, run_id, a["name"])
break
if build_url is None:
raise BuildInfoNotFound("unable to find a build url for the"
" changeset %r" % changeset)
raise BuildInfoNotFound(
"unable to find a build url for the" " changeset %r" % changeset
)
return IntegrationBuildInfo(
self.fetch_config,
build_url=build_url,
@ -194,21 +193,21 @@ class NightlyInfoFetcher(InfoFetcher):
build info file are found for the url.
"""
data = {}
if not url.endswith('/'):
url += '/'
if not url.endswith("/"):
url += "/"
for link in url_links(url):
if 'build_url' not in data and self.build_regex.match(link):
data['build_url'] = url + link
elif 'build_txt_url' not in data \
and self.build_info_regex.match(link):
data['build_txt_url'] = url + link
if "build_url" not in data and self.build_regex.match(link):
data["build_url"] = url + link
elif "build_txt_url" not in data and self.build_info_regex.match(link):
data["build_txt_url"] = url + link
if data:
# Check that we found all required data. The URL in build_url is
# required. build_txt_url is optional.
if 'build_url' not in data:
if "build_url" not in data:
raise BuildInfoNotFound(
"Failed to find a build file in directory {} that "
"matches regex '{}'".format(url, self.build_regex.pattern))
"matches regex '{}'".format(url, self.build_regex.pattern)
)
with self._fetch_lock:
lst.append((index, data))
@ -262,9 +261,10 @@ class NightlyInfoFetcher(InfoFetcher):
valid_builds = []
while build_urls:
some = build_urls[:max_workers]
threads = [Thread(target=self._fetch_build_info_from_url,
args=(url, i, valid_builds))
for i, url in enumerate(some)]
threads = [
Thread(target=self._fetch_build_info_from_url, args=(url, i, valid_builds))
for i, url in enumerate(some)
]
for thread in threads:
thread.daemon = True
thread.start()
@ -279,10 +279,10 @@ class NightlyInfoFetcher(InfoFetcher):
build_info = NightlyBuildInfo(
self.fetch_config,
build_url=infos['build_url'],
build_url=infos["build_url"],
build_date=date,
changeset=infos.get('changeset'),
repo_url=infos.get('repository')
changeset=infos.get("changeset"),
repo_url=infos.get("repository"),
)
break
build_urls = build_urls[max_workers:]

Просмотреть файл

@ -20,31 +20,28 @@ an instance of :class:`ClassRegistry`. Example: ::
print REGISTRY.names()
"""
from __future__ import absolute_import
import re
import datetime
from mozregression.class_registry import ClassRegistry
from mozregression import errors, branches
from mozregression.dates import to_utc_timestamp
from mozregression.config import ARCHIVE_BASE_URL
import datetime
import re
from abc import ABCMeta, abstractmethod
import six
from mozregression import branches, errors
from mozregression.class_registry import ClassRegistry
from mozregression.config import ARCHIVE_BASE_URL
from mozregression.dates import to_utc_timestamp
# switch from fennec api-11 to api-15 on taskcluster
# appeared on this date for m-c.
TIMESTAMP_FENNEC_API_15 = to_utc_timestamp(
datetime.datetime(2016, 1, 29, 0, 30, 13)
)
TIMESTAMP_FENNEC_API_15 = to_utc_timestamp(datetime.datetime(2016, 1, 29, 0, 30, 13))
# switch from fennec api-15 to api-16 on taskcluster
# appeared on this date for m-c.
TIMESTAMP_FENNEC_API_16 = to_utc_timestamp(
datetime.datetime(2017, 8, 29, 18, 28, 36)
)
TIMESTAMP_FENNEC_API_16 = to_utc_timestamp(datetime.datetime(2017, 8, 29, 18, 28, 36))
def get_build_regex(name, os, bits, processor, psuffix='', with_ext=True):
def get_build_regex(name, os, bits, processor, psuffix="", with_ext=True):
"""
Returns a string regexp that can match a build filename.
@ -75,15 +72,14 @@ def get_build_regex(name, os, bits, processor, psuffix='', with_ext=True):
suffix, ext = r".*mac.*", r"\.dmg"
else:
raise errors.MozRegressionError(
"mozregression supports linux, mac and windows but your"
" os is reported as '%s'." % os
"mozregression supports linux, mac and windows but your" " os is reported as '%s'." % os
)
# New taskcluster builds now just name the binary archive 'target', so
# that is added as one possibility in the regex.
regex = '(target|%s%s%s)' % (name, suffix, psuffix)
regex = "(target|%s%s%s)" % (name, suffix, psuffix)
if with_ext:
return '%s%s' % (regex, ext)
return "%s%s" % (regex, ext)
else:
return regex
@ -92,7 +88,8 @@ class CommonConfig(object):
"""
Define the configuration for both nightly and integration fetching.
"""
BUILD_TYPES = ('opt',) # only opt allowed by default
BUILD_TYPES = ("opt",) # only opt allowed by default
BUILD_TYPE_FALLBACKS = {}
app_name = None
@ -101,7 +98,7 @@ class CommonConfig(object):
self.bits = bits
self.processor = processor
self.repo = None
self.set_build_type('opt')
self.set_build_type("opt")
self._used_build_index = 0
@property
@ -119,23 +116,25 @@ class CommonConfig(object):
"""
self._used_build_index = (
# Need to be careful not to overflow the list
(self._used_build_index + 1) % len(self.build_types)
(self._used_build_index + 1)
% len(self.build_types)
)
def build_regex(self):
"""
Returns a string regex that can match a build file on the servers.
"""
return get_build_regex(self.app_name, self.os, self.bits,
self.processor) + '$'
return get_build_regex(self.app_name, self.os, self.bits, self.processor) + "$"
def build_info_regex(self):
"""
Returns a string regex that can match a build info file (txt)
on the servers.
"""
return get_build_regex(self.app_name, self.os, self.bits,
self.processor, with_ext=False) + r'\.txt$'
return (
get_build_regex(self.app_name, self.os, self.bits, self.processor, with_ext=False)
+ r"\.txt$"
)
def available_bits(self):
"""
@ -149,13 +148,10 @@ class CommonConfig(object):
for available in self.BUILD_TYPES:
match = re.match(r"(.+)\[(.+)\]", available)
if match:
suffix = ('-aarch64' if self.processor == 'aarch64' and
self.bits == 64 else '')
suffix = "-aarch64" if self.processor == "aarch64" and self.bits == 64 else ""
available = match.group(1)
platforms = match.group(2)
if '{}{}{}'.format(
self.os, self.bits, suffix
) not in platforms.split(','):
if "{}{}{}".format(self.os, self.bits, suffix) not in platforms.split(","):
available = None
if available:
res.append(available)
@ -169,9 +165,7 @@ class CommonConfig(object):
"""
if build_type in self.available_build_types():
fallbacks = self.BUILD_TYPE_FALLBACKS.get(build_type)
self.build_types = (
(build_type,) + fallbacks if fallbacks else (build_type,)
)
self.build_types = (build_type,) + fallbacks if fallbacks else (build_type,)
return
raise errors.MozRegressionError(
"Unable to find a suitable build type %r." % str(build_type)
@ -194,11 +188,11 @@ class CommonConfig(object):
Note that this method relies on the repo and build type defined.
"""
return not (branches.get_category(self.repo) in
('integration', 'try', 'releases') or
# we can find the asan builds (firefox and jsshell) in
# archives.m.o
self.build_type not in ('opt', 'asan', 'shippable'))
# we can find the asan builds (firefox and jsshell) in archives.m.o
return not (
branches.get_category(self.repo) in ("integration", "try", "releases")
or self.build_type not in ("opt", "asan", "shippable")
)
class NightlyConfigMixin(six.with_metaclass(ABCMeta)):
@ -216,21 +210,24 @@ class NightlyConfigMixin(six.with_metaclass(ABCMeta)):
Note that subclasses must implement :meth:`_get_nightly_repo` to
provide a default value.
"""
archive_base_url = ARCHIVE_BASE_URL
nightly_base_repo_name = "firefox"
nightly_repo = None
def set_base_url(self, url):
self.archive_base_url = url.rstrip('/')
self.archive_base_url = url.rstrip("/")
def get_nighly_base_url(self, date):
"""
Returns the base part of the nightly build url for a given date.
"""
return "%s/%s/nightly/%04d/%02d/" % (self.archive_base_url,
self.nightly_base_repo_name,
date.year,
date.month)
return "%s/%s/nightly/%04d/%02d/" % (
self.archive_base_url,
self.nightly_base_repo_name,
date.year,
date.month,
)
def get_nightly_repo(self, date):
"""
@ -256,11 +253,16 @@ class NightlyConfigMixin(six.with_metaclass(ABCMeta)):
def _get_nightly_repo_regex(self, date, repo):
if isinstance(date, datetime.datetime):
return (r'^%04d-%02d-%02d-%02d-%02d-%02d-%s/$'
% (date.year, date.month, date.day, date.hour,
date.minute, date.second, repo))
return (r'^%04d-%02d-%02d-[\d-]+%s/$'
% (date.year, date.month, date.day, repo))
return r"^%04d-%02d-%02d-%02d-%02d-%02d-%s/$" % (
date.year,
date.month,
date.day,
date.hour,
date.minute,
date.second,
repo,
)
return r"^%04d-%02d-%02d-[\d-]+%s/$" % (date.year, date.month, date.day, repo)
class FirefoxNightlyConfigMixin(NightlyConfigMixin):
@ -272,7 +274,7 @@ class FirefoxNightlyConfigMixin(NightlyConfigMixin):
class ThunderbirdNightlyConfigMixin(NightlyConfigMixin):
nightly_base_repo_name = 'thunderbird'
nightly_base_repo_name = "thunderbird"
def _get_nightly_repo(self, date):
# sneaking this in here
@ -294,11 +296,11 @@ class FennecNightlyConfigMixin(NightlyConfigMixin):
nightly_base_repo_name = "mobile"
def _get_nightly_repo(self, date):
return 'mozilla-central'
return "mozilla-central"
def get_nightly_repo_regex(self, date):
repo = self.get_nightly_repo(date)
if repo in ('mozilla-central',):
if repo in ("mozilla-central",):
if date < datetime.date(2014, 12, 6):
repo += "-android"
elif date < datetime.date(2014, 12, 13):
@ -316,7 +318,8 @@ class IntegrationConfigMixin(six.with_metaclass(ABCMeta)):
"""
Define the integration-related required configuration.
"""
default_integration_branch = 'mozilla-central'
default_integration_branch = "mozilla-central"
_tk_credentials = None
@property
@ -342,7 +345,7 @@ class IntegrationConfigMixin(six.with_metaclass(ABCMeta)):
builds. Returns an empty string by default, or 'debug' if build type
is debug.
"""
return self.build_type if self.build_type != 'opt' else ''
return self.build_type if self.build_type != "opt" else ""
def tk_needs_auth(self):
"""
@ -362,77 +365,76 @@ class IntegrationConfigMixin(six.with_metaclass(ABCMeta)):
Returns the takcluster options, including the credentials required to
download private artifacts.
"""
tk_options = {'rootUrl': root_url}
tk_options = {"rootUrl": root_url}
if self.tk_needs_auth():
tk_options.update({'credentials': self._tk_credentials})
tk_options.update({"credentials": self._tk_credentials})
return tk_options
def _common_tk_part(integration_conf):
# private method to avoid copy/paste for building taskcluster route part.
if integration_conf.os == 'linux':
part = 'linux'
if integration_conf.os == "linux":
part = "linux"
if integration_conf.bits == 64:
part += str(integration_conf.bits)
elif integration_conf.os == 'mac':
part = 'macosx64'
elif integration_conf.os == "mac":
part = "macosx64"
else:
# windows
part = '{}{}'.format(integration_conf.os, integration_conf.bits)
if integration_conf.processor == 'aarch64' and integration_conf.bits == 64:
part += '-aarch64'
part = "{}{}".format(integration_conf.os, integration_conf.bits)
if integration_conf.processor == "aarch64" and integration_conf.bits == 64:
part += "-aarch64"
return part
class FirefoxIntegrationConfigMixin(IntegrationConfigMixin):
def tk_routes(self, push):
for build_type in self.build_types:
yield 'gecko.v2.{}{}.revision.{}.firefox.{}-{}'.format(
yield "gecko.v2.{}{}.revision.{}.firefox.{}-{}".format(
self.integration_branch,
'.shippable' if build_type == 'shippable' else '',
".shippable" if build_type == "shippable" else "",
push.changeset,
_common_tk_part(self),
'opt' if build_type == 'shippable' else build_type
"opt" if build_type == "shippable" else build_type,
)
self._inc_used_build()
return
class FennecIntegrationConfigMixin(IntegrationConfigMixin):
tk_name = 'android-api-11'
tk_name = "android-api-11"
def tk_routes(self, push):
tk_name = self.tk_name
if tk_name == 'android-api-11':
if tk_name == "android-api-11":
if push.timestamp >= TIMESTAMP_FENNEC_API_16:
tk_name = 'android-api-16'
tk_name = "android-api-16"
elif push.timestamp >= TIMESTAMP_FENNEC_API_15:
tk_name = 'android-api-15'
tk_name = "android-api-15"
for build_type in self.build_types:
yield 'gecko.v2.{}.revision.{}.mobile.{}-{}'.format(
self.integration_branch, push.changeset, tk_name,
build_type
yield "gecko.v2.{}.revision.{}.mobile.{}-{}".format(
self.integration_branch, push.changeset, tk_name, build_type
)
self._inc_used_build()
return
class ThunderbirdIntegrationConfigMixin(IntegrationConfigMixin):
default_integration_branch = 'comm-central'
default_integration_branch = "comm-central"
def tk_routes(self, push):
for build_type in self.build_types:
yield 'comm.v2.{}.revision.{}.thunderbird.{}-{}'.format(
self.integration_branch, push.changeset, _common_tk_part(self),
build_type
yield "comm.v2.{}.revision.{}.thunderbird.{}-{}".format(
self.integration_branch, push.changeset, _common_tk_part(self), build_type,
)
self._inc_used_build()
return
# ------------ full config implementations ------------
REGISTRY = ClassRegistry('app_name')
REGISTRY = ClassRegistry("app_name")
def create_config(name, os, bits, processor):
@ -449,62 +451,68 @@ def create_config(name, os, bits, processor):
return REGISTRY.get(name)(os, bits, processor)
@REGISTRY.register('firefox')
class FirefoxConfig(CommonConfig,
FirefoxNightlyConfigMixin,
FirefoxIntegrationConfigMixin):
BUILD_TYPES = ('shippable', 'opt', 'pgo[linux32,linux64,win32,win64]',
'debug', 'asan[linux64]', 'asan-debug[linux64]')
@REGISTRY.register("firefox")
class FirefoxConfig(CommonConfig, FirefoxNightlyConfigMixin, FirefoxIntegrationConfigMixin):
BUILD_TYPES = (
"shippable",
"opt",
"pgo[linux32,linux64,win32,win64]",
"debug",
"asan[linux64]",
"asan-debug[linux64]",
)
BUILD_TYPE_FALLBACKS = {
'shippable': ('opt', 'pgo'),
'opt': ('shippable', 'pgo'),
"shippable": ("opt", "pgo"),
"opt": ("shippable", "pgo"),
}
def __init__(self, os, bits, processor):
super(FirefoxConfig, self).__init__(os, bits, processor)
self.set_build_type('shippable')
self.set_build_type("shippable")
def build_regex(self):
return get_build_regex(
self.app_name, self.os, self.bits, self.processor,
psuffix='-asan-reporter' if 'asan' in self.build_type else ''
) + '$'
return (
get_build_regex(
self.app_name,
self.os,
self.bits,
self.processor,
psuffix="-asan-reporter" if "asan" in self.build_type else "",
)
+ "$"
)
@REGISTRY.register('thunderbird')
class ThunderbirdConfig(CommonConfig,
ThunderbirdNightlyConfigMixin,
ThunderbirdIntegrationConfigMixin):
@REGISTRY.register("thunderbird")
class ThunderbirdConfig(
CommonConfig, ThunderbirdNightlyConfigMixin, ThunderbirdIntegrationConfigMixin
):
pass
@REGISTRY.register('fennec')
class FennecConfig(CommonConfig,
FennecNightlyConfigMixin,
FennecIntegrationConfigMixin):
BUILD_TYPES = ('opt', 'debug')
@REGISTRY.register("fennec")
class FennecConfig(CommonConfig, FennecNightlyConfigMixin, FennecIntegrationConfigMixin):
BUILD_TYPES = ("opt", "debug")
def build_regex(self):
return r'(target|fennec-.*)\.apk'
return r"(target|fennec-.*)\.apk"
def build_info_regex(self):
return r'(target|fennec-.*)\.txt'
return r"(target|fennec-.*)\.txt"
def available_bits(self):
return ()
@REGISTRY.register('gve')
class GeckoViewExampleConfig(CommonConfig,
FennecNightlyConfigMixin,
FennecIntegrationConfigMixin):
BUILD_TYPES = ('opt', 'debug')
@REGISTRY.register("gve")
class GeckoViewExampleConfig(CommonConfig, FennecNightlyConfigMixin, FennecIntegrationConfigMixin):
BUILD_TYPES = ("opt", "debug")
def build_regex(self):
return r'geckoview_example\.apk'
return r"geckoview_example\.apk"
def build_info_regex(self):
return r'(target|fennec-.*)\.txt'
return r"(target|fennec-.*)\.txt"
def available_bits(self):
return ()
@ -514,13 +522,13 @@ class GeckoViewExampleConfig(CommonConfig,
return False
@REGISTRY.register('fennec-2.3', attr_value='fennec')
@REGISTRY.register("fennec-2.3", attr_value="fennec")
class Fennec23Config(FennecConfig):
tk_name = 'android-api-9'
tk_name = "android-api-9"
def get_nightly_repo_regex(self, date):
repo = self.get_nightly_repo(date)
if repo == 'mozilla-central':
if repo == "mozilla-central":
if date < datetime.date(2014, 12, 6):
repo = "mozilla-central-android"
else:
@ -528,28 +536,30 @@ class Fennec23Config(FennecConfig):
return self._get_nightly_repo_regex(date, repo)
@REGISTRY.register('jsshell', disable_in_gui=True)
@REGISTRY.register("jsshell", disable_in_gui=True)
class JsShellConfig(FirefoxConfig):
def build_info_regex(self):
# the info file is the one for firefox
return get_build_regex('firefox', self.os, self.bits, self.processor,
with_ext=False) + r'\.txt$'
return (
get_build_regex("firefox", self.os, self.bits, self.processor, with_ext=False)
+ r"\.txt$"
)
def build_regex(self):
if self.os == 'linux':
if self.os == "linux":
if self.bits == 64:
part = 'linux-x86_64'
part = "linux-x86_64"
else:
part = 'linux-i686'
elif self.os == 'win':
part = "linux-i686"
elif self.os == "win":
if self.bits == 64:
if self.processor == "aarch64":
part = 'win64-aarch64'
part = "win64-aarch64"
else:
part = 'win64(-x86_64)?'
part = "win64(-x86_64)?"
else:
part = 'win32'
part = "win32"
else:
part = 'mac'
psuffix = '-asan' if 'asan' in self.build_type else ''
return r'jsshell-%s%s\.zip$' % (part, psuffix)
part = "mac"
psuffix = "-asan" if "asan" in self.build_type else ""
return r"jsshell-%s%s\.zip$" % (part, psuffix)

Просмотреть файл

@ -3,10 +3,10 @@ Representation of the bisection history.
"""
from __future__ import absolute_import
from collections import namedtuple
BisectionStep = namedtuple('BisectionStep', 'build_range, index, verdict')
BisectionStep = namedtuple("BisectionStep", "build_range, index, verdict")
class BisectionHistory(list):
@ -20,5 +20,6 @@ class BisectionHistory(list):
since it only store steps for one handler - e.g only for
one branch.
"""
def add(self, build_range, index, verdict):
self.append(BisectionStep(build_range, index, verdict))

Просмотреть файл

@ -1,13 +1,14 @@
from __future__ import absolute_import
import datetime
import six
from mozlog import get_proxy_logger
from mozregression.errors import EmptyPushlogError
from mozregression.network import retry_get
from mozregression import branches
from mozregression.dates import is_date_or_datetime
import six
from mozregression.errors import EmptyPushlogError
from mozregression.network import retry_get
LOG = get_proxy_logger("JsonPushes")
@ -16,7 +17,8 @@ class Push(object):
"""
Simple wrapper around a json push object from json-pushes API.
"""
__slots__ = ('_data', '_push_id') # to save memory usage
__slots__ = ("_data", "_push_id") # to save memory usage
def __init__(self, push_id, data):
self._data = data
@ -28,18 +30,18 @@ class Push(object):
@property
def changesets(self):
return self._data['changesets']
return self._data["changesets"]
@property
def changeset(self):
"""
Returns the last changeset in the push (the most interesting for us)
"""
return self._data['changesets'][-1]
return self._data["changesets"][-1]
@property
def timestamp(self):
return self._data['date']
return self._data["date"]
@property
def utc_date(self):
@ -53,7 +55,8 @@ class JsonPushes(object):
"""
Find pushlog Push objects from a mozilla hg json-pushes api.
"""
def __init__(self, branch='mozilla-central'):
def __init__(self, branch="mozilla-central"):
self.branch = branch
self.repo_url = branches.get_url(branch)
@ -63,15 +66,19 @@ class JsonPushes(object):
Basically issue a raw request to the server.
"""
base_url = '%s/json-pushes?' % self.repo_url
url = base_url + '&'.join(sorted("%s=%s" % kv for kv in six.iteritems(kwargs)))
base_url = "%s/json-pushes?" % self.repo_url
url = base_url + "&".join(sorted("%s=%s" % kv for kv in six.iteritems(kwargs)))
LOG.debug("Using url: %s" % url)
response = retry_get(url)
data = response.json()
if (response.status_code == 404 and data is not None and
"error" in data and "unknown revision" in data["error"]):
if (
response.status_code == 404
and data is not None
and "error" in data
and "unknown revision" in data["error"]
):
raise EmptyPushlogError(
"The url %r returned a 404 error because the push is not"
" in this repo (e.g., not merged yet)." % url
@ -80,8 +87,7 @@ class JsonPushes(object):
if not data:
raise EmptyPushlogError(
"The url %r contains no pushlog. Maybe use another range ?"
% url
"The url %r contains no pushlog. Maybe use another range ?" % url
)
pushlog = []
@ -89,8 +95,7 @@ class JsonPushes(object):
pushlog.append(Push(key, data[key]))
return pushlog
def pushes_within_changes(self, fromchange, tochange, verbose=True,
**kwargs):
def pushes_within_changes(self, fromchange, tochange, verbose=True, **kwargs):
"""
Returns a list of Push objects, including fromchange and tochange.
@ -105,17 +110,16 @@ class JsonPushes(object):
# the first changeset is not taken into account in the result.
# let's add it directly with this request
chsets = self.pushes(changeset=fromchange)
kwargs['fromchange'] = fromchange
kwargs["fromchange"] = fromchange
else:
chsets = []
kwargs['startdate'] = fromchange.strftime('%Y-%m-%d')
kwargs["startdate"] = fromchange.strftime("%Y-%m-%d")
if not to_is_date:
kwargs['tochange'] = tochange
kwargs["tochange"] = tochange
else:
# add one day to take the last day in account
kwargs['enddate'] = (
tochange + datetime.timedelta(days=1)).strftime('%Y-%m-%d')
kwargs["enddate"] = (tochange + datetime.timedelta(days=1)).strftime("%Y-%m-%d")
# now fetch all remaining changesets
chsets.extend(self.pushes(**kwargs))
@ -123,12 +127,18 @@ class JsonPushes(object):
log = LOG.info if verbose else LOG.debug
if from_is_date:
first = chsets[0]
log("Using {} (pushed on {}) for date {}".format(
first.changeset, first.utc_date, fromchange))
log(
"Using {} (pushed on {}) for date {}".format(
first.changeset, first.utc_date, fromchange
)
)
if to_is_date:
last = chsets[-1]
log("Using {} (pushed on {}) for date {}".format(
last.changeset, last.utc_date, tochange))
log(
"Using {} (pushed on {}) for date {}".format(
last.changeset, last.utc_date, tochange
)
)
return chsets
@ -140,12 +150,9 @@ class JsonPushes(object):
"""
if is_date_or_datetime(changeset):
try:
return self.pushes_within_changes(changeset,
changeset,
verbose=False)[-1]
return self.pushes_within_changes(changeset, changeset, verbose=False)[-1]
except EmptyPushlogError:
raise EmptyPushlogError(
"No pushes available for the date %s on %s."
% (changeset, self.branch)
"No pushes available for the date %s on %s." % (changeset, self.branch)
)
return self.pushes(changeset=changeset, **kwargs)[0]

Просмотреть файл

@ -6,29 +6,30 @@
Define the launcher classes, responsible of running the tested applications.
"""
from __future__ import absolute_import
from __future__ import print_function
from __future__ import absolute_import, print_function
import json
import os
import time
from mozlog.structured import get_default_logger, get_proxy_logger
from mozprofile import ThunderbirdProfile, Profile
from mozrunner import Runner
from mozfile import remove
from mozdevice import ADBAndroid, ADBHost, ADBError
import mozversion
import mozinstall
import zipfile
import mozinfo
import six
import sys
import stat
from subprocess import call
import sys
import time
import zipfile
from abc import ABCMeta, abstractmethod
from subprocess import call
from threading import Thread
import mozinfo
import mozinstall
import mozversion
import six
from mozdevice import ADBAndroid, ADBError, ADBHost
from mozfile import remove
from mozlog.structured import get_default_logger, get_proxy_logger
from mozprofile import Profile, ThunderbirdProfile
from mozrunner import Runner
from mozregression.class_registry import ClassRegistry
from mozregression.errors import LauncherNotRunnable, LauncherError
from mozregression.errors import LauncherError, LauncherNotRunnable
from mozregression.tempdir import safe_mkdtemp
LOG = get_proxy_logger("Test Runner")
@ -39,6 +40,7 @@ class Launcher(six.with_metaclass(ABCMeta)):
Handle the logic of downloading a build file, installing and
running an application.
"""
profile_class = Profile
@classmethod
@ -134,18 +136,13 @@ class Launcher(six.with_metaclass(ABCMeta)):
if isinstance(profile, Profile):
return profile
else:
return self.create_profile(profile=profile, addons=addons,
preferences=preferences)
return self.create_profile(profile=profile, addons=addons, preferences=preferences)
@classmethod
def create_profile(cls, profile=None, addons=(), preferences=None,
clone=True):
def create_profile(cls, profile=None, addons=(), preferences=None, clone=True):
if profile:
if not os.path.exists(profile):
LOG.warning(
"Creating directory '%s' to put the profile in there"
% profile
)
LOG.warning("Creating directory '%s' to put the profile in there" % profile)
os.makedirs(profile)
# since the user gave an empty dir for the profile,
# let's keep it on the disk in any case.
@ -155,14 +152,11 @@ class Launcher(six.with_metaclass(ABCMeta)):
# be undone. Let's clone the profile to not have side effect
# on existing profile.
# see https://bugzilla.mozilla.org/show_bug.cgi?id=999009
profile = cls.profile_class.clone(profile, addons=addons,
preferences=preferences)
profile = cls.profile_class.clone(profile, addons=addons, preferences=preferences)
else:
profile = cls.profile_class(profile, addons=addons,
preferences=preferences)
profile = cls.profile_class(profile, addons=addons, preferences=preferences)
elif len(addons):
profile = cls.profile_class(addons=addons,
preferences=preferences)
profile = cls.profile_class(addons=addons, preferences=preferences)
else:
profile = cls.profile_class(preferences=preferences)
return profile
@ -181,52 +175,41 @@ def safe_get_version(**kwargs):
class MozRunnerLauncher(Launcher):
tempdir = None
runner = None
app_name = 'undefined'
app_name = "undefined"
binary = None
def _install(self, dest):
self.tempdir = safe_mkdtemp()
try:
self.binary = mozinstall.get_binary(
mozinstall.install(src=dest, dest=self.tempdir),
self.app_name
mozinstall.install(src=dest, dest=self.tempdir), self.app_name
)
except Exception:
remove(self.tempdir)
raise
def _disableUpdateByPolicy(self):
updatePolicy = {
'policies': {
'DisableAppUpdate': True
}
}
updatePolicy = {"policies": {"DisableAppUpdate": True}}
installdir = os.path.dirname(self.binary)
if mozinfo.os == 'mac':
if mozinfo.os == "mac":
# macOS has the following filestructure:
# binary at:
# PackageName.app/Contents/MacOS/firefox
# we need policies.json in:
# PackageName.app/Contents/Resources/distribution
installdir = os.path.normpath(
os.path.join(installdir, '..', 'Resources')
)
os.mkdir(os.path.join(installdir, 'distribution'))
policyFile = os.path.join(
installdir, 'distribution', 'policies.json'
)
with open(policyFile, 'w') as fp:
installdir = os.path.normpath(os.path.join(installdir, "..", "Resources"))
os.mkdir(os.path.join(installdir, "distribution"))
policyFile = os.path.join(installdir, "distribution", "policies.json")
with open(policyFile, "w") as fp:
json.dump(updatePolicy, fp, indent=2)
def _start(self, profile=None, addons=(), cmdargs=(), preferences=None,
adb_profile_dir=None):
profile = self._create_profile(profile=profile, addons=addons,
preferences=preferences)
def _start(
self, profile=None, addons=(), cmdargs=(), preferences=None, adb_profile_dir=None,
):
profile = self._create_profile(profile=profile, addons=addons, preferences=preferences)
LOG.info("Launching %s" % self.binary)
self.runner = Runner(binary=self.binary,
cmdargs=cmdargs,
profile=profile)
self.runner = Runner(binary=self.binary, cmdargs=cmdargs, profile=profile)
def _on_exit():
# if we are stopping the process do not log anything.
@ -243,23 +226,22 @@ class MozRunnerLauncher(Launcher):
except Exception:
print()
LOG.error(
"Error while waiting process, consider filing a bug.",
exc_info=True
"Error while waiting process, consider filing a bug.", exc_info=True,
)
return
if exitcode != 0:
# first print a blank line, to be sure we don't
# write on an already printed line without EOL.
print()
LOG.warning('Process exited with code %s' % exitcode)
LOG.warning("Process exited with code %s" % exitcode)
# we don't need stdin, and GUI will not work in Windowed mode if set
# see: https://stackoverflow.com/a/40108817
devnull = open(os.devnull, 'wb')
devnull = open(os.devnull, "wb")
self.runner.process_args = {
'processOutputLine': [get_default_logger("process").info],
'stdin': devnull,
'onFinish': _on_exit,
"processOutputLine": [get_default_logger("process").info],
"stdin": devnull,
"onFinish": _on_exit,
}
self.runner.start()
@ -267,7 +249,7 @@ class MozRunnerLauncher(Launcher):
return self.runner.wait()
def _stop(self):
if mozinfo.os == "win" and self.app_name == 'firefox':
if mozinfo.os == "win" and self.app_name == "firefox":
# for some reason, stopping the runner may hang on windows. For
# example restart the browser in safe mode, it will hang for a
# couple of minutes. As a workaround, we call that in a thread and
@ -294,17 +276,14 @@ class MozRunnerLauncher(Launcher):
return safe_get_version(binary=self.binary)
REGISTRY = ClassRegistry('app_name')
REGISTRY = ClassRegistry("app_name")
def create_launcher(buildinfo):
"""
Create and returns an instance launcher for the given buildinfo.
"""
return REGISTRY.get(buildinfo.app_name)(
buildinfo.build_file,
task_id=buildinfo.task_id
)
return REGISTRY.get(buildinfo.app_name)(buildinfo.build_file, task_id=buildinfo.task_id)
class FirefoxRegressionProfile(Profile):
@ -317,39 +296,38 @@ class FirefoxRegressionProfile(Profile):
preferences = {
# Don't automatically update the application (only works on older
# versions of Firefox)
'app.update.enabled': False,
"app.update.enabled": False,
# On newer versions of Firefox (where disabling automatic updates
# is impossible, at least don't update automatically)
'app.update.auto': False,
"app.update.auto": False,
# Don't automatically download the update (this pref is specific to
# some versions of Fennec)
'app.update.autodownload': 'disabled',
"app.update.autodownload": "disabled",
# Don't restore the last open set of tabs
# if the browser has crashed
'browser.sessionstore.resume_from_crash': False,
"browser.sessionstore.resume_from_crash": False,
# Don't check for the default web browser during startup
'browser.shell.checkDefaultBrowser': False,
"browser.shell.checkDefaultBrowser": False,
# Don't warn on exit when multiple tabs are open
'browser.tabs.warnOnClose': False,
"browser.tabs.warnOnClose": False,
# Don't warn when exiting the browser
'browser.warnOnQuit': False,
"browser.warnOnQuit": False,
# Don't send Firefox health reports to the production
# server
'datareporting.healthreport.uploadEnabled': False,
'datareporting.healthreport.documentServerURI':
'http://%(server)s/healthreport/',
"datareporting.healthreport.uploadEnabled": False,
"datareporting.healthreport.documentServerURI": "http://%(server)s/healthreport/",
# Don't show tab with privacy notice on every launch
'datareporting.policy.dataSubmissionPolicyBypassNotification': True,
"datareporting.policy.dataSubmissionPolicyBypassNotification": True,
# Don't report telemetry information
'toolkit.telemetry.enabled': False,
"toolkit.telemetry.enabled": False,
# Allow sideloading extensions
'extensions.autoDisableScopes': 0,
"extensions.autoDisableScopes": 0,
# Disable what's new page
'browser.startup.homepage_override.mstone': 'ignore',
"browser.startup.homepage_override.mstone": "ignore",
}
@REGISTRY.register('firefox')
@REGISTRY.register("firefox")
class FirefoxLauncher(MozRunnerLauncher):
profile_class = FirefoxRegressionProfile
@ -357,11 +335,12 @@ class FirefoxLauncher(MozRunnerLauncher):
super(FirefoxLauncher, self)._install(dest)
self._disableUpdateByPolicy()
def _start(self, profile=None, addons=(), cmdargs=(), preferences=None,
adb_profile_dir=None):
super(FirefoxLauncher, self)._start(profile, addons,
['--allow-downgrade'] + cmdargs,
preferences, adb_profile_dir)
def _start(
self, profile=None, addons=(), cmdargs=(), preferences=None, adb_profile_dir=None,
):
super(FirefoxLauncher, self)._start(
profile, addons, ["--allow-downgrade"] + cmdargs, preferences, adb_profile_dir,
)
class ThunderbirdRegressionProfile(ThunderbirdProfile):
@ -371,12 +350,12 @@ class ThunderbirdRegressionProfile(ThunderbirdProfile):
preferences = {
# Don't automatically update the application
'app.update.enabled': False,
'app.update.auto': False,
"app.update.enabled": False,
"app.update.auto": False,
}
@REGISTRY.register('thunderbird')
@REGISTRY.register("thunderbird")
class ThunderbirdLauncher(MozRunnerLauncher):
profile_class = ThunderbirdRegressionProfile
@ -403,39 +382,36 @@ class AndroidLauncher(Launcher):
except ADBError as adb_error:
raise LauncherNotRunnable(str(adb_error))
if not devices:
raise LauncherNotRunnable("No android device connected."
" Connect a device and try again.")
raise LauncherNotRunnable(
"No android device connected." " Connect a device and try again."
)
def _install(self, dest):
# get info now, as dest may be removed
self.app_info = safe_get_version(binary=dest)
self.package_name = self.app_info.get("package_name",
self._get_package_name())
self.package_name = self.app_info.get("package_name", self._get_package_name())
self.adb = ADBAndroid(require_root=False)
try:
self.adb.uninstall_app(self.package_name)
except ADBError as msg:
LOG.warning(
"Failed to uninstall %s (%s)\nThis is normal if it is the"
" first time the application is installed."
% (self.package_name, msg)
" first time the application is installed." % (self.package_name, msg)
)
self.adb.install_app(dest)
def _start(self, profile=None, addons=(), cmdargs=(), preferences=None,
adb_profile_dir=None):
def _start(
self, profile=None, addons=(), cmdargs=(), preferences=None, adb_profile_dir=None,
):
# for now we don't handle addons on the profile for fennec
profile = self._create_profile(profile=profile,
preferences=preferences)
profile = self._create_profile(profile=profile, preferences=preferences)
# send the profile on the device
if not adb_profile_dir:
adb_profile_dir = self.adb.test_root
self.remote_profile = "/".join([adb_profile_dir,
os.path.basename(profile.profile)])
self.remote_profile = "/".join([adb_profile_dir, os.path.basename(profile.profile)])
if self.adb.exists(self.remote_profile):
self.adb.rm(self.remote_profile, recursive=True)
LOG.debug("Pushing profile to device (%s -> %s)" % (
profile.profile, self.remote_profile))
LOG.debug("Pushing profile to device (%s -> %s)" % (profile.profile, self.remote_profile))
self.adb.push(profile.profile, self.remote_profile)
self._launch()
@ -452,31 +428,32 @@ class AndroidLauncher(Launcher):
return self.app_info
@REGISTRY.register('fennec')
@REGISTRY.register("fennec")
class FennecLauncher(AndroidLauncher):
def _get_package_name(self):
return "org.mozilla.fennec"
def _launch(self):
LOG.debug("Launching fennec")
self.adb.launch_fennec(self.package_name,
extra_args=["-profile", self.remote_profile])
self.adb.launch_fennec(self.package_name, extra_args=["-profile", self.remote_profile])
@REGISTRY.register('gve')
@REGISTRY.register("gve")
class GeckoViewExampleLauncher(AndroidLauncher):
def _get_package_name(self):
return "org.mozilla.geckoview_example"
def _launch(self):
LOG.debug("Launching geckoview_example")
self.adb.launch_activity(self.package_name,
activity_name="GeckoViewActivity",
extra_args=["-profile", self.remote_profile],
e10s=True)
self.adb.launch_activity(
self.package_name,
activity_name="GeckoViewActivity",
extra_args=["-profile", self.remote_profile],
e10s=True,
)
@REGISTRY.register('jsshell')
@REGISTRY.register("jsshell")
class JsShellLauncher(Launcher):
temp_dir = None
@ -485,10 +462,7 @@ class JsShellLauncher(Launcher):
try:
with zipfile.ZipFile(dest, "r") as z:
z.extractall(self.tempdir)
self.binary = os.path.join(
self.tempdir,
'js' if mozinfo.os != 'win' else 'js.exe'
)
self.binary = os.path.join(self.tempdir, "js" if mozinfo.os != "win" else "js.exe")
# set the file executable
os.chmod(self.binary, os.stat(self.binary).st_mode | stat.S_IEXEC)
except Exception:
@ -499,7 +473,7 @@ class JsShellLauncher(Launcher):
LOG.info("Launching %s" % self.binary)
res = call([self.binary], cwd=self.tempdir)
if res != 0:
LOG.warning('jsshell exited with code %d.' % res)
LOG.warning("jsshell exited with code %d." % res)
def _wait(self):
pass

Просмотреть файл

@ -3,14 +3,15 @@ Logging and outputting configuration and utilities.
"""
from __future__ import absolute_import
import sys
import time
import mozinfo
from colorama import Fore, Style, Back
from mozlog.structuredlog import set_default_logger, StructuredLogger
from mozlog.handlers import StreamHandler, LogLevelFilter
import mozinfo
import six
from colorama import Back, Fore, Style
from mozlog.handlers import LogLevelFilter, StreamHandler
from mozlog.structuredlog import StructuredLogger, set_default_logger
ALLOW_COLOR = sys.stdout.isatty()
@ -18,7 +19,7 @@ ALLOW_COLOR = sys.stdout.isatty()
def _format_seconds(total):
"""Format number of seconds to MM:SS.DD form."""
minutes, seconds = divmod(total, 60)
return '%2d:%05.2f' % (minutes, seconds)
return "%2d:%05.2f" % (minutes, seconds)
def init_logger(debug=True, allow_color=ALLOW_COLOR, output=None):
@ -29,31 +30,30 @@ def init_logger(debug=True, allow_color=ALLOW_COLOR, output=None):
output = output or sys.stdout
start = time.time() * 1000
level_color = {
'WARNING': Fore.MAGENTA + Style.BRIGHT,
'CRITICAL': Fore.RED + Style.BRIGHT,
'ERROR': Fore.RED + Style.BRIGHT,
'DEBUG': Fore.CYAN + Style.BRIGHT,
'INFO': Style.BRIGHT,
"WARNING": Fore.MAGENTA + Style.BRIGHT,
"CRITICAL": Fore.RED + Style.BRIGHT,
"ERROR": Fore.RED + Style.BRIGHT,
"DEBUG": Fore.CYAN + Style.BRIGHT,
"INFO": Style.BRIGHT,
}
time_color = Fore.BLUE
if mozinfo.os == "win":
time_color += Style.BRIGHT # this is unreadable on windows without it
def format_log(data):
level = data['level']
elapsed = _format_seconds((data['time'] - start) / 1000)
level = data["level"]
elapsed = _format_seconds((data["time"] - start) / 1000)
if allow_color:
elapsed = time_color + elapsed + Style.RESET_ALL
if level in level_color:
level = level_color[level] + level + Style.RESET_ALL
msg = data['message']
if 'stack' in data:
msg += "\n%s" % data['stack']
msg = data["message"]
if "stack" in data:
msg += "\n%s" % data["stack"]
return "%s %s: %s\n" % (elapsed, level, msg)
logger = StructuredLogger("mozregression")
handler = LogLevelFilter(StreamHandler(output, format_log),
'debug' if debug else 'info')
handler = LogLevelFilter(StreamHandler(output, format_log), "debug" if debug else "info")
logger.add_handler(handler)
set_default_logger(logger)
@ -63,10 +63,10 @@ def init_logger(debug=True, allow_color=ALLOW_COLOR, output=None):
COLORS = {}
NO_COLORS = {}
for prefix, st in (('b', Back), ('s', Style), ('f', Fore)):
for prefix, st in (("b", Back), ("s", Style), ("f", Fore)):
for name, value in six.iteritems(st.__dict__):
COLORS[prefix + name] = value
NO_COLORS[prefix + name] = ''
NO_COLORS[prefix + name] = ""
def colorize(text, allow_color=ALLOW_COLOR):

Просмотреть файл

@ -10,12 +10,13 @@ break mach!
"""
from __future__ import absolute_import
from argparse import Namespace
from mozregression import __version__
from mozregression.cli import create_parser
from mozregression.config import DEFAULT_CONF_FNAME, get_defaults
from mozregression.main import main, pypi_latest_version
from mozregression import __version__
def new_release_on_pypi():

Просмотреть файл

@ -7,34 +7,33 @@ Entry point for the mozregression command line.
"""
from __future__ import absolute_import
import os
import sys
import requests
import atexit
import pipes
import mozfile
import colorama
import atexit
import os
import pipes
import sys
import colorama
import mozfile
import requests
from mozlog import get_proxy_logger
from requests.exceptions import RequestException, HTTPError
from requests.exceptions import HTTPError, RequestException
from mozregression import __version__
from mozregression.config import TC_CREDENTIALS_FNAME, DEFAULT_EXPAND
from mozregression.approx_persist import ApproxPersistChooser
from mozregression.bisector import Bisection, Bisector, IntegrationHandler, NightlyHandler
from mozregression.bugzilla import bug_url, find_bugids_in_push
from mozregression.cli import cli
from mozregression.errors import MozRegressionError, GoodBadExpectationError
from mozregression.bisector import (Bisector, NightlyHandler, IntegrationHandler,
Bisection)
from mozregression.config import DEFAULT_EXPAND, TC_CREDENTIALS_FNAME
from mozregression.download_manager import BuildDownloadManager
from mozregression.errors import GoodBadExpectationError, MozRegressionError
from mozregression.fetch_build_info import IntegrationInfoFetcher, NightlyInfoFetcher
from mozregression.json_pushes import JsonPushes
from mozregression.launchers import REGISTRY as APP_REGISTRY
from mozregression.network import set_http_session
from mozregression.tempdir import safe_mkdtemp
from mozregression.test_runner import ManualTestRunner, CommandTestRunner
from mozregression.download_manager import BuildDownloadManager
from mozregression.persist_limit import PersistLimit
from mozregression.fetch_build_info import (NightlyInfoFetcher,
IntegrationInfoFetcher)
from mozregression.json_pushes import JsonPushes
from mozregression.bugzilla import find_bugids_in_push, bug_url
from mozregression.approx_persist import ApproxPersistChooser
from mozregression.tempdir import safe_mkdtemp
from mozregression.test_runner import CommandTestRunner, ManualTestRunner
LOG = get_proxy_logger("main")
@ -55,16 +54,16 @@ class Application(object):
launcher_class.check_is_runnable()
# init global profile if required
self._global_profile = None
if options.profile_persistence in ('clone-first', 'reuse'):
if options.profile_persistence in ("clone-first", "reuse"):
self._global_profile = launcher_class.create_profile(
profile=options.profile,
addons=options.addons,
preferences=options.preferences,
clone=options.profile_persistence == 'clone-first'
clone=options.profile_persistence == "clone-first",
)
options.cmdargs = options.cmdargs + ['--allow-downgrade']
options.cmdargs = options.cmdargs + ["--allow-downgrade"]
elif options.profile:
options.cmdargs = options.cmdargs + ['--allow-downgrade']
options.cmdargs = options.cmdargs + ["--allow-downgrade"]
def clear(self):
if self._build_download_manager:
@ -79,21 +78,22 @@ class Application(object):
# https://bugzilla.mozilla.org/show_bug.cgi?id=1231745
self._build_download_manager.wait(raise_if_error=False)
mozfile.remove(self._download_dir)
if self._global_profile \
and self.options.profile_persistence == 'clone-first':
if self._global_profile and self.options.profile_persistence == "clone-first":
self._global_profile.cleanup()
@property
def test_runner(self):
if self._test_runner is None:
if self.options.command is None:
self._test_runner = ManualTestRunner(launcher_kwargs=dict(
addons=self.options.addons,
profile=self._global_profile or self.options.profile,
cmdargs=self.options.cmdargs,
preferences=self.options.preferences,
adb_profile_dir=self.options.adb_profile_dir,
))
self._test_runner = ManualTestRunner(
launcher_kwargs=dict(
addons=self.options.addons,
profile=self._global_profile or self.options.profile,
cmdargs=self.options.cmdargs,
preferences=self.options.preferences,
adb_profile_dir=self.options.adb_profile_dir,
)
)
else:
self._test_runner = CommandTestRunner(self.options.command)
return self._test_runner
@ -102,11 +102,13 @@ class Application(object):
def bisector(self):
if self._bisector is None:
self._bisector = Bisector(
self.fetch_config, self.test_runner,
self.fetch_config,
self.test_runner,
self.build_download_manager,
dl_in_background=self.options.background_dl,
approx_chooser=(None if self.options.approx_policy != 'auto'
else ApproxPersistChooser(7)),
approx_chooser=(
None if self.options.approx_policy != "auto" else ApproxPersistChooser(7)
),
)
return self._bisector
@ -120,7 +122,7 @@ class Application(object):
self._build_download_manager = BuildDownloadManager(
self._download_dir,
background_dl_policy=background_dl_policy,
persist_limit=PersistLimit(self.options.persist_size_limit)
persist_limit=PersistLimit(self.options.persist_size_limit),
)
return self._build_download_manager
@ -128,25 +130,26 @@ class Application(object):
good_date, bad_date = self.options.good, self.options.bad
handler = NightlyHandler(
find_fix=self.options.find_fix,
ensure_good_and_bad=self.options.mode != 'no-first-check',
ensure_good_and_bad=self.options.mode != "no-first-check",
)
result = self._do_bisect(handler, good_date, bad_date)
if result == Bisection.FINISHED:
LOG.info("Got as far as we can go bisecting nightlies...")
handler.print_range()
LOG.info("Switching bisection method to taskcluster")
self.fetch_config.set_repo(
self.fetch_config.get_nightly_repo(handler.bad_date))
return self._bisect_integration(handler.good_revision,
handler.bad_revision,
expand=DEFAULT_EXPAND)
self.fetch_config.set_repo(self.fetch_config.get_nightly_repo(handler.bad_date))
return self._bisect_integration(
handler.good_revision, handler.bad_revision, expand=DEFAULT_EXPAND
)
elif result == Bisection.USER_EXIT:
self._print_resume_info(handler)
else:
# NO_DATA
LOG.info("Unable to get valid builds within the given"
" range. You should try to launch mozregression"
" again with a larger date range.")
LOG.info(
"Unable to get valid builds within the given"
" range. You should try to launch mozregression"
" again with a larger date range."
)
return 1
return 0
@ -154,15 +157,17 @@ class Application(object):
return self._bisect_integration(
self.options.good,
self.options.bad,
ensure_good_and_bad=self.options.mode != 'no-first-check',
ensure_good_and_bad=self.options.mode != "no-first-check",
)
def _bisect_integration(self, good_rev, bad_rev, ensure_good_and_bad=False,
expand=0):
LOG.info("Getting %s builds between %s and %s"
% (self.fetch_config.integration_branch, good_rev, bad_rev))
handler = IntegrationHandler(find_fix=self.options.find_fix,
ensure_good_and_bad=ensure_good_and_bad)
def _bisect_integration(self, good_rev, bad_rev, ensure_good_and_bad=False, expand=0):
LOG.info(
"Getting %s builds between %s and %s"
% (self.fetch_config.integration_branch, good_rev, bad_rev)
)
handler = IntegrationHandler(
find_fix=self.options.find_fix, ensure_good_and_bad=ensure_good_and_bad
)
result = self._do_bisect(handler, good_rev, bad_rev, expand=expand)
if result == Bisection.FINISHED:
LOG.info("No more integration revisions, bisection finished.")
@ -179,8 +184,7 @@ class Application(object):
if result:
branch, good_rev, bad_rev = result
self.fetch_config.set_repo(branch)
return self._bisect_integration(good_rev, bad_rev,
expand=DEFAULT_EXPAND)
return self._bisect_integration(good_rev, bad_rev, expand=DEFAULT_EXPAND)
else:
# This code is broken, it prints out the message even when
# there are multiple bug numbers or commits in the range.
@ -191,38 +195,42 @@ class Application(object):
# just missing the builds for some intermediate builds)
# (2) there is only one bug number in that push
jp = JsonPushes(handler.build_range[1].repo_name)
num_pushes = len(jp.pushes_within_changes(
handler.build_range[0].changeset,
handler.build_range[1].changeset))
num_pushes = len(
jp.pushes_within_changes(
handler.build_range[0].changeset, handler.build_range[1].changeset,
)
)
if num_pushes == 2:
bugids = find_bugids_in_push(
handler.build_range[1].repo_name,
handler.build_range[1].changeset
handler.build_range[1].repo_name, handler.build_range[1].changeset,
)
if len(bugids) == 1:
word = 'fix' if handler.find_fix else 'regression'
LOG.info("Looks like the following bug has the "
" changes which introduced the"
" {}:\n{}".format(word,
bug_url(bugids[0])))
word = "fix" if handler.find_fix else "regression"
LOG.info(
"Looks like the following bug has the "
" changes which introduced the"
" {}:\n{}".format(word, bug_url(bugids[0]))
)
elif result == Bisection.USER_EXIT:
self._print_resume_info(handler)
else:
# NO_DATA. With integration branches, this can not happen if changesets
# are incorrect - so builds are probably too old
LOG.info(
'There are no build artifacts for these changesets (they are probably too old).')
"There are no build artifacts for these changesets (they are probably too old)."
)
return 1
return 0
def _do_bisect(self, handler, good, bad, **kwargs):
try:
return self.bisector.bisect(handler, good, bad, **kwargs)
except (KeyboardInterrupt, MozRegressionError,
RequestException) as exc:
if handler.good_revision is not None and \
handler.bad_revision is not None and \
not isinstance(exc, GoodBadExpectationError):
except (KeyboardInterrupt, MozRegressionError, RequestException) as exc:
if (
handler.good_revision is not None
and handler.bad_revision is not None
and not isinstance(exc, GoodBadExpectationError)
):
atexit.register(self._on_exit_print_resume_info, handler)
raise
@ -230,8 +238,7 @@ class Application(object):
# copy sys.argv, remove every --good/--bad/--repo related argument,
# then add our own
argv = sys.argv[:]
args = ('--good', '--bad', '-g', '-b', '--good-rev', '--bad-rev',
'--repo')
args = ("--good", "--bad", "-g", "-b", "--good-rev", "--bad-rev", "--repo")
indexes_to_remove = []
for i, arg in enumerate(argv):
if i in indexes_to_remove:
@ -241,24 +248,24 @@ class Application(object):
# handle '--good 2015-01-01'
indexes_to_remove.extend((i, i + 1))
break
elif arg.startswith(karg + '='):
elif arg.startswith(karg + "="):
# handle '--good=2015-01-01'
indexes_to_remove.append(i)
break
for i in reversed(indexes_to_remove):
del argv[i]
argv.append('--repo=%s' % handler.build_range[0].repo_name)
argv.append("--repo=%s" % handler.build_range[0].repo_name)
if hasattr(handler, 'good_date'):
argv.append('--good=%s' % handler.good_date)
argv.append('--bad=%s' % handler.bad_date)
if hasattr(handler, "good_date"):
argv.append("--good=%s" % handler.good_date)
argv.append("--bad=%s" % handler.bad_date)
else:
argv.append('--good=%s' % handler.good_revision)
argv.append('--bad=%s' % handler.bad_revision)
argv.append("--good=%s" % handler.good_revision)
argv.append("--bad=%s" % handler.bad_revision)
LOG.info('To resume, run:')
LOG.info(' '.join([pipes.quote(arg) for arg in argv]))
LOG.info("To resume, run:")
LOG.info(" ".join([pipes.quote(arg) for arg in argv]))
def _on_exit_print_resume_info(self, handler):
handler.print_range()
@ -279,7 +286,7 @@ class Application(object):
def pypi_latest_version():
url = "https://pypi.python.org/pypi/mozregression/json"
return requests.get(url, timeout=10).json()['info']['version']
return requests.get(url, timeout=10).json()["info"]["version"]
def check_mozregression_version():
@ -290,12 +297,15 @@ def check_mozregression_version():
return
if __version__ != mozregression_version:
LOG.warning("You are using mozregression version %s, "
"however version %s is available."
% (__version__, mozregression_version))
LOG.warning(
"You are using mozregression version %s, "
"however version %s is available." % (__version__, mozregression_version)
)
LOG.warning("You should consider upgrading via the 'pip install"
" --upgrade mozregression' command.")
LOG.warning(
"You should consider upgrading via the 'pip install"
" --upgrade mozregression' command."
)
def main(argv=None, namespace=None, check_new_version=True):
@ -303,7 +313,7 @@ def main(argv=None, namespace=None, check_new_version=True):
main entry point of mozregression command line.
"""
# terminal color support on windows
if os.name == 'nt':
if os.name == "nt":
colorama.init()
if sys.version_info <= (2, 7, 9):
@ -311,6 +321,7 @@ def main(argv=None, namespace=None, check_new_version=True):
# of warnings that we do not want. See
# https://bugzilla.mozilla.org/show_bug.cgi?id=1199020
import logging
logging.captureWarnings(True)
config, app = None, None

Просмотреть файл

@ -7,12 +7,13 @@ network functions utilities for mozregression.
"""
from __future__ import absolute_import
import re
import redo
import requests
from bs4 import BeautifulSoup
import six
from bs4 import BeautifulSoup
def retry_get(url, **karwgs):
@ -23,10 +24,14 @@ def retry_get(url, **karwgs):
it will retry the requests call three times in case of HTTPError or
ConnectionError.
"""
return redo.retry(get_http_session().get, attempts=3, sleeptime=1,
retry_exceptions=(requests.exceptions.HTTPError,
requests.exceptions.ConnectionError),
args=(url,), kwargs=karwgs)
return redo.retry(
get_http_session().get,
attempts=3,
sleeptime=1,
retry_exceptions=(requests.exceptions.HTTPError, requests.exceptions.ConnectionError,),
args=(url,),
kwargs=karwgs,
)
SESSION = None
@ -53,6 +58,7 @@ def set_http_session(session=None, get_defaults=None):
for k, v in six.iteritems(get_defaults):
kwargs.setdefault(k, v)
return _get(*args, **kwargs)
session.get = _default_get
SESSION = session
@ -85,19 +91,20 @@ def url_links(url, regex=None, auth=None):
regex = re.compile(regex)
match = regex.match
else:
def match(_):
return True
# do not return a generator but an array, so we can store it for later use
result = []
for link in soup.findAll('a'):
href = link.get('href')
for link in soup.findAll("a"):
href = link.get("href")
# return "relative" part of the url only
if href.startswith('/'):
if href.endswith('/'):
href = href.strip('/').rsplit('/', 1)[-1] + '/'
if href.startswith("/"):
if href.endswith("/"):
href = href.strip("/").rsplit("/", 1)[-1] + "/"
else:
href = href.rsplit('/', 1)[-1]
href = href.rsplit("/", 1)[-1]
if match(href):
result.append(href)
return result

Просмотреть файл

@ -3,14 +3,15 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
from __future__ import absolute_import
import os
import stat
import mozfile
from glob import glob
from collections import namedtuple
from glob import glob
import mozfile
File = namedtuple('File', ('path', 'stat'))
File = namedtuple("File", ("path", "stat"))
class PersistLimit(object):
@ -25,6 +26,7 @@ class PersistLimit(object):
:param file_limit: even if the size limit is reached, this force
to keep at least *file_limit* files.
"""
def __init__(self, size_limit, file_limit=5):
self.size_limit = size_limit
self.file_limit = file_limit
@ -60,8 +62,7 @@ class PersistLimit(object):
return
# sort by creation time, oldest first
files = sorted(self.files, key=lambda f: f.stat.st_atime)
while len(files) > self.file_limit and \
self._files_size >= self.size_limit:
while len(files) > self.file_limit and self._files_size >= self.size_limit:
f = files.pop(0)
mozfile.remove(f.path)
self._files_size -= f.stat.st_size

Просмотреть файл

@ -1,12 +1,13 @@
from __future__ import absolute_import
import re
import re
from datetime import date
import six
from six.moves import filter, map
from mozregression.errors import UnavailableRelease
from mozregression.network import retry_get
import six
from six.moves import filter
from six.moves import map
def releases():
@ -71,7 +72,7 @@ def releases():
53: "2017-01-23",
54: "2017-03-06",
55: "2017-06-12",
56: "2017-08-02"
56: "2017-08-02",
}
def filter_tags(tag_node):
@ -89,10 +90,7 @@ def releases():
response = retry_get(tags_url)
if response.status_code == 200:
fetched_releases = list(map(
map_tags,
list(filter(filter_tags, response.json()["tags"]))
))
fetched_releases = list(map(map_tags, list(filter(filter_tags, response.json()["tags"]))))
for release in fetched_releases:
releases.update(release)
@ -114,10 +112,10 @@ def tag_of_release(release):
"""
Provide the mercurial tag of a release, suitable for use in place of a hash
"""
if re.match(r'^\d+$', release):
release += '.0'
if re.match(r'^\d+\.\d(\.\d)?$', release):
return 'FIREFOX_%s_RELEASE' % release.replace('.', '_')
if re.match(r"^\d+$", release):
release += ".0"
if re.match(r"^\d+\.\d(\.\d)?$", release):
return "FIREFOX_%s_RELEASE" % release.replace(".", "_")
else:
raise UnavailableRelease(release)
@ -127,10 +125,10 @@ def tag_of_beta(release):
Provide the mercurial tag of a beta release, suitable for use in place of a
hash
"""
if re.match(r'^\d+\.0b\d+$', release):
return 'FIREFOX_%s_RELEASE' % release.replace('.', '_')
elif re.match(r'^\d+(\.0)?$', release):
return 'FIREFOX_RELEASE_%s_BASE' % release.replace('.0', '')
if re.match(r"^\d+\.0b\d+$", release):
return "FIREFOX_%s_RELEASE" % release.replace(".", "_")
elif re.match(r"^\d+(\.0)?$", release):
return "FIREFOX_RELEASE_%s_BASE" % release.replace(".0", "")
else:
raise UnavailableRelease(release)
@ -142,6 +140,6 @@ def formatted_valid_release_dates():
"""
message = "Valid releases: \n"
for key, value in six.iteritems(releases()):
message += '% 3s: %s\n' % (key, value)
message += "% 3s: %s\n" % (key, value)
return message

Просмотреть файл

@ -3,11 +3,12 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
from __future__ import absolute_import
import json
from taskcluster import utils as tc_utils
from mozregression.config import (get_defaults, DEFAULT_CONF_FNAME,
TC_CREDENTIALS_FNAME)
from mozregression.config import DEFAULT_CONF_FNAME, TC_CREDENTIALS_FNAME, get_defaults
def tc_authenticate(logger):
@ -16,8 +17,8 @@ def tc_authenticate(logger):
"""
# first, try to load credentials from mozregression config file
defaults = get_defaults(DEFAULT_CONF_FNAME)
client_id = defaults.get('taskcluster-clientid')
access_token = defaults.get('taskcluster-accesstoken')
client_id = defaults.get("taskcluster-clientid")
access_token = defaults.get("taskcluster-accesstoken")
if client_id and access_token:
return dict(clientId=client_id, accessToken=access_token)
@ -25,7 +26,7 @@ def tc_authenticate(logger):
# else, try to load a valid certificate locally
with open(TC_CREDENTIALS_FNAME) as f:
creds = json.load(f)
if not tc_utils.isExpired(creds['certificate']):
if not tc_utils.isExpired(creds["certificate"]):
return creds
except Exception:
pass
@ -41,6 +42,6 @@ def tc_authenticate(logger):
creds = tc_utils.authenticate("mozregression private build access")
# save the credentials and the certificate for later use
with open(TC_CREDENTIALS_FNAME, 'w') as f:
with open(TC_CREDENTIALS_FNAME, "w") as f:
json.dump(creds, f)
return creds

Просмотреть файл

@ -2,20 +2,23 @@
# 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
import os
import tempfile
import six
def safe_mkdtemp():
'''
"""
Creates a temporary directory using mkdtemp, but makes sure that the
returned directory is the full path on windows (see:
https://bugzilla.mozilla.org/show_bug.cgi?id=1385928)
'''
"""
tempdir = tempfile.mkdtemp()
if os.name == 'nt':
if os.name == "nt":
from ctypes import create_unicode_buffer, windll
BUFFER_SIZE = 500
buffer = create_unicode_buffer(BUFFER_SIZE)
get_long_path_name = windll.kernel32.GetLongPathNameW

Просмотреть файл

@ -7,20 +7,20 @@ This module implements a :class:`TestRunner` interface for testing builds
and a default implementation :class:`ManualTestRunner`.
"""
from __future__ import absolute_import
from __future__ import print_function
from mozlog import get_proxy_logger
import subprocess
import shlex
import os
import datetime
from __future__ import absolute_import, print_function
from mozregression.launchers import create_launcher as mozlauncher
from mozregression.errors import TestCommandError, LauncherError
import datetime
import os
import shlex
import subprocess
from abc import ABCMeta, abstractmethod
import six
from six.moves import range
from six.moves import input
from mozlog import get_proxy_logger
from six.moves import input, range
from mozregression.errors import LauncherError, TestCommandError
from mozregression.launchers import create_launcher as mozlauncher
LOG = get_proxy_logger("Test Runner")
@ -29,16 +29,13 @@ def create_launcher(build_info):
"""
Create and returns a :class:`mozregression.launchers.Launcher`.
"""
if build_info.build_type == 'nightly':
if build_info.build_type == "nightly":
if isinstance(build_info.build_date, datetime.datetime):
desc = ("for buildid %s"
% build_info.build_date.strftime("%Y%m%d%H%M%S"))
desc = "for buildid %s" % build_info.build_date.strftime("%Y%m%d%H%M%S")
else:
desc = "for %s" % build_info.build_date
else:
desc = ("built on %s, revision %s"
% (build_info.build_date,
build_info.short_changeset))
desc = "built on %s, revision %s" % (build_info.build_date, build_info.short_changeset,)
LOG.info("Running %s build %s" % (build_info.repo_name, desc))
return mozlauncher(build_info)
@ -93,6 +90,7 @@ class ManualTestRunner(TestRunner):
A TestRunner subclass that run builds and ask for evaluation by
prompting in the terminal.
"""
def __init__(self, launcher_kwargs=None):
TestRunner.__init__(self)
self.launcher_kwargs = launcher_kwargs or {}
@ -101,22 +99,22 @@ class ManualTestRunner(TestRunner):
"""
Ask and returns the verdict.
"""
options = ['good', 'bad', 'skip', 'retry', 'exit']
options = ["good", "bad", "skip", "retry", "exit"]
if allow_back:
options.insert(-1, 'back')
options.insert(-1, "back")
# allow user to just type one letter
allowed_inputs = options + [o[0] for o in options]
# format options to nice printing
formatted_options = (', '.join(["'%s'" % o for o in options[:-1]]) +
" or '%s'" % options[-1])
formatted_options = ", ".join(["'%s'" % o for o in options[:-1]]) + " or '%s'" % options[-1]
verdict = ""
while verdict not in allowed_inputs:
verdict = input("Was this %s build good, bad, or broken?"
" (type %s and press Enter): " % (build_info.build_type,
formatted_options))
verdict = input(
"Was this %s build good, bad, or broken?"
" (type %s and press Enter): " % (build_info.build_type, formatted_options)
)
if verdict == 'back':
return 'back'
if verdict == "back":
return "back"
# shorten verdict to one character for processing...
return verdict[0]
@ -149,13 +147,17 @@ class ManualTestRunner(TestRunner):
min = -mid + 1
max = build_range_len - mid - 2
valid_range = list(range(min, max + 1))
print("Build was skipped. You can manually choose a new build to"
" test, to be able to get out of a broken build range.")
print("Please type the index of the build you would like to try - the"
" index is 0-based on the middle of the remaining build range.")
print(
"Build was skipped. You can manually choose a new build to"
" test, to be able to get out of a broken build range."
)
print(
"Please type the index of the build you would like to try - the"
" index is 0-based on the middle of the remaining build range."
)
print("You can choose a build index between [%d, %d]:" % (min, max))
while True:
value = input('> ')
value = input("> ")
try:
index = int(value)
if index in valid_range:
@ -164,9 +166,8 @@ class ManualTestRunner(TestRunner):
pass
def _raise_command_error(exc, msg=''):
raise TestCommandError("Unable to run the test command%s: `%s`"
% (msg, exc))
def _raise_command_error(exc, msg=""):
raise TestCommandError("Unable to run the test command%s: `%s`" % (msg, exc))
class CommandTestRunner(TestRunner):
@ -185,6 +186,7 @@ class CommandTestRunner(TestRunner):
with curly brackets. Example:
`mozmill -app firefox -b {binary} -t path/to/test.js`
"""
def __init__(self, command):
TestRunner.__init__(self)
self.command = command
@ -193,29 +195,28 @@ class CommandTestRunner(TestRunner):
with create_launcher(build_info) as launcher:
build_info.update_from_app_info(launcher.get_app_info())
variables = {k: v for k, v in six.iteritems(build_info.to_dict())}
if hasattr(launcher, 'binary'):
variables['binary'] = launcher.binary
if hasattr(launcher, "binary"):
variables["binary"] = launcher.binary
env = dict(os.environ)
for k, v in six.iteritems(variables):
env['MOZREGRESSION_' + k.upper()] = str(v)
env["MOZREGRESSION_" + k.upper()] = str(v)
try:
command = self.command.format(**variables)
except KeyError as exc:
_raise_command_error(exc, ' (formatting error)')
LOG.info('Running test command: `%s`' % command)
_raise_command_error(exc, " (formatting error)")
LOG.info("Running test command: `%s`" % command)
cmdlist = shlex.split(command)
try:
retcode = subprocess.call(cmdlist, env=env)
except IndexError:
_raise_command_error("Empty command")
except OSError as exc:
_raise_command_error(exc,
" (%s not found or not executable)"
% cmdlist[0])
LOG.info('Test command result: %d (build is %s)'
% (retcode, 'good' if retcode == 0 else 'bad'))
return 'g' if retcode == 0 else 'b'
_raise_command_error(exc, " (%s not found or not executable)" % cmdlist[0])
LOG.info(
"Test command result: %d (build is %s)" % (retcode, "good" if retcode == 0 else "bad")
)
return "g" if retcode == 0 else "b"
def run_once(self, build_info):
return 0 if self.evaluate(build_info) == 'g' else 1
return 0 if self.evaluate(build_info) == "g" else 1

14
pyproject.toml Normal file
Просмотреть файл

@ -0,0 +1,14 @@
[tool.black]
line-length = 100
exclude = "gui/mozregui/ui"
[tool.isort]
line_length = 100
skip_glob = "**/gui/mozregui/ui/*"
default_section = "THIRDPARTY"
known_first_party = "mozregression,mozregui"
# For compatibility with black:
multi_line_output = 3
include_trailing_comma = "True"
force_grid_wrap = 0
use_parentheses = "True"

6
requirements/all.txt Normal file
Просмотреть файл

@ -0,0 +1,6 @@
# all the things (what you usually want when doing development)
-r dev.txt
-r linters.txt
-r build.txt
-r gui.txt
-e .

Просмотреть файл

@ -5,6 +5,7 @@ configobj==5.0.6
mozdevice==3.1.0
mozfile==2.1.0
mozinfo==1.2.1
mozinstall==2.0.0
mozlog==6.0
mozprofile==2.5.0
mozrunner==7.8.0
@ -15,16 +16,3 @@ taskcluster==6.0.0
# for some reason we need to specify a specific version of six
six==1.12.0
# dev dependencies
coverage==4.2
mock==3.0.5 # last version compatible with python2.7
pytest==4.6.9 # last version compatible with python2.7
pytest-mock == 2.0.0
# peg flake8 + deps so code doesn't suddenly violate validation expectations unexpectedly
flake8==3.2.1
mccabe==0.5.3
pyflakes==1.3.0
# install mozregression
-e .

4
requirements/console.txt Normal file
Просмотреть файл

@ -0,0 +1,4 @@
# *just* the dependencies required for the console version of
# mozregression (compatible with older versions of python)
-r build.txt
-r dev.txt

4
requirements/dev.txt Normal file
Просмотреть файл

@ -0,0 +1,4 @@
coverage==4.2
mock==3.0.5 # last version compatible with python2.7
pytest==4.6.9 # last version compatible with python2.7
pytest-mock == 2.0.0

Просмотреть файл

@ -1,5 +1,3 @@
-r requirements-dev.txt
# Qt bindings for python
PySide2==5.14.1

6
requirements/linters.txt Normal file
Просмотреть файл

@ -0,0 +1,6 @@
# peg flake8 + deps so code doesn't suddenly violate validation expectations unexpectedly
flake8==3.7.9
flake8-black==0.1.1
isort==4.3.21
mccabe==0.6.1
pyflakes==2.1.1

Просмотреть файл

@ -1,6 +1,10 @@
[flake8]
exclude = .git,__pycache__
# E129: visually indented line with same indent as next logical line
# E501: line too long
extend_ignore = E129,E501
exclude = .git,__pycache__,vendor,gui/mozregui/ui
max-line-length = 100
# E121,E123,E126,E226,E24,E704,W503: Ignored in default pycodestyle config:
# https://github.com/PyCQA/pycodestyle/blob/2.2.0/pycodestyle.py#L72
# Our additions...
# E129: visually indented line with same indent as next logical line
# E203: pep8 is wrong, overridden by black (https://github.com/psf/black/issues/315)
ignore = E121,E123,E126,E129,E203,E226,E24,E704,W503

Просмотреть файл

@ -1,25 +1,29 @@
import sys
from setuptools import setup
from mozregression import __version__
from setuptools.command.test import test as TestCommand
from mozregression import __version__
class PyTest(TestCommand):
"""
Run py.test with the "python setup.py test command"
"""
user_options = [('pytest-args=', 'a', "Arguments to pass to py.test")]
user_options = [("pytest-args=", "a", "Arguments to pass to py.test")]
def initialize_options(self):
TestCommand.initialize_options(self)
self.pytest_args = ''
self.pytest_args = ""
def finalize_options(self):
TestCommand.finalize_options(self)
self.pytest_args += (' ' + self.distribution.test_suite)
self.pytest_args += " " + self.distribution.test_suite
def run_tests(self):
import pytest
errno = pytest.main(self.pytest_args)
sys.exit(errno)
@ -30,22 +34,22 @@ if sys.version_info < (2, 7) or (sys.version_info >= (3, 0) and sys.version_info
# we pin these dependencies in the requirements files -- all of these
# should be python 3 compatible
DEPENDENCIES = [
'beautifulsoup4>=4.7.1',
'colorama>=0.4.1',
'configobj>=5.0.6',
'mozdevice>=3.0.1',
'mozfile>=2.0.0',
'mozinfo>=1.1.0',
'mozinstall>=2.0.0',
'mozlog>=4.0',
'mozprocess>=1.0.0',
'mozprofile>=2.2.0',
'mozrunner>=7.4.0',
'mozversion>=2.1.0',
'redo>=2.0.2',
'requests[security]>=2.21.0',
'six>=1.12.0',
'taskcluster>=6.0.0',
"beautifulsoup4>=4.7.1",
"colorama>=0.4.1",
"configobj>=5.0.6",
"mozdevice>=3.0.1",
"mozfile>=2.0.0",
"mozinfo>=1.1.0",
"mozinstall>=2.0.0",
"mozlog>=4.0",
"mozprocess>=1.0.0",
"mozprofile>=2.2.0",
"mozrunner>=7.4.0",
"mozversion>=2.1.0",
"redo>=2.0.2",
"requests[security]>=2.21.0",
"six>=1.12.0",
"taskcluster>=6.0.0",
]
desc = """Regression range finder for Mozilla nightly builds"""
@ -53,24 +57,26 @@ long_desc = """Regression range finder for Mozilla nightly builds.
For more information see the mozregression website:
http://mozilla.github.io/mozregression/"""
setup(name="mozregression",
version=__version__,
description=desc,
long_description=long_desc,
author='Mozilla Automation and Tools Team',
author_email='tools@lists.mozilla.org',
url='http://github.com/mozilla/mozregression',
license='MPL 1.1/GPL 2.0/LGPL 2.1',
packages=['mozregression'],
entry_points="""
setup(
name="mozregression",
version=__version__,
description=desc,
long_description=long_desc,
author="Mozilla Automation and Tools Team",
author_email="tools@lists.mozilla.org",
url="http://github.com/mozilla/mozregression",
license="MPL 1.1/GPL 2.0/LGPL 2.1",
packages=["mozregression"],
entry_points="""
[console_scripts]
mozregression = mozregression.main:main
""",
platforms=['Any'],
install_requires=DEPENDENCIES,
classifiers=[
'Development Status :: 5 - Production/Stable',
'Environment :: Console',
'Intended Audience :: Developers',
'Operating System :: OS Independent'
])
platforms=["Any"],
install_requires=DEPENDENCIES,
classifiers=[
"Development Status :: 5 - Production/Stable",
"Environment :: Console",
"Intended Audience :: Developers",
"Operating System :: OS Independent",
],
)

Просмотреть файл

@ -1,6 +1,6 @@
from __future__ import absolute_import
from mozlog.structured import set_default_logger
from mozlog.structured.structuredlog import StructuredLogger
set_default_logger(StructuredLogger('mozregression.tests.unit'))
set_default_logger(StructuredLogger("mozregression.tests.unit"))

20
tests/unit/conftest.py Normal file
Просмотреть файл

@ -0,0 +1,20 @@
import pytest
from mozregression import build_range
from mozregression.fetch_build_info import InfoFetcher
class RangeCreator(object):
def __init__(self, mocker):
self.mocker = mocker
def create(self, values):
info_fetcher = self.mocker.Mock(spec=InfoFetcher)
info_fetcher.find_build_info.side_effect = lambda i: i
future_build_infos = [build_range.FutureBuildInfo(info_fetcher, v) for v in values]
return build_range.BuildRange(info_fetcher, future_build_infos)
@pytest.fixture
def range_creator(mocker):
return RangeCreator(mocker)

Просмотреть файл

@ -1,8 +1,10 @@
from __future__ import absolute_import
import pytest
from .test_build_info import create_build_info
from mozregression import build_info, approx_persist, build_range
import pytest
from mozregression import approx_persist, build_info, build_range
from .test_build_info import create_build_info
def create_build_range(values):
@ -10,47 +12,47 @@ def create_build_range(values):
data = []
for v in values:
data.append(build_range.FutureBuildInfo(
info_fetcher, v
))
data.append(build_range.FutureBuildInfo(info_fetcher, v))
return build_range.BuildRange(info_fetcher, data)
def build_firefox_name(chset):
return ('%s-shippable--mozilla-central--firefox-38.0a1.en-US.linux-x86_64.tar.bz2'
% chset)
return "%s-shippable--mozilla-central--firefox-38.0a1.en-US.linux-x86_64.tar.bz2" % chset
def build_firefox_names(chsets):
return [build_firefox_name(c) for c in chsets]
@pytest.mark.parametrize('bdata, mid, around, fnames, result', [
# index is None when there is no files
('0123456789', None, 7, [], None),
# one file around works
('0123456789', None, 7, build_firefox_names('4'), 4),
('0123456789', None, 7, build_firefox_names('6'), 6),
# with 10 builds, two files around returns None
('0123456789', None, 7, build_firefox_names('123789'), None),
# same with 13
('0123456789abc', None, 7, build_firefox_names('8'), None),
# but 14 will give a result
('0123456789abcd', None, 7, build_firefox_names('8'), 8),
# we never overflow
('0123456789', 8, 4, [], None),
('0123456789', 1, 4, [], None),
# it is possible that someone chooses '1' after a skip.
# in that case, we should not offer the build '0' (the first)
('0123456789', 1, 7, build_firefox_names('0'), None),
# same thing with '8' (here, we do not provide the last)
('0123456789', 8, 7, build_firefox_names('9'), None),
# though if it is neither the last or the first, we use it
('0123456789', 1, 7, build_firefox_names('2'), 2),
('0123456789', 2, 7, build_firefox_names('1'), 1),
('0123456789', 8, 7, build_firefox_names('7'), 7),
('0123456789', 7, 7, build_firefox_names('8'), 8),
])
@pytest.mark.parametrize(
"bdata, mid, around, fnames, result",
[
# index is None when there is no files
("0123456789", None, 7, [], None),
# one file around works
("0123456789", None, 7, build_firefox_names("4"), 4),
("0123456789", None, 7, build_firefox_names("6"), 6),
# with 10 builds, two files around returns None
("0123456789", None, 7, build_firefox_names("123789"), None),
# same with 13
("0123456789abc", None, 7, build_firefox_names("8"), None),
# but 14 will give a result
("0123456789abcd", None, 7, build_firefox_names("8"), 8),
# we never overflow
("0123456789", 8, 4, [], None),
("0123456789", 1, 4, [], None),
# it is possible that someone chooses '1' after a skip.
# in that case, we should not offer the build '0' (the first)
("0123456789", 1, 7, build_firefox_names("0"), None),
# same thing with '8' (here, we do not provide the last)
("0123456789", 8, 7, build_firefox_names("9"), None),
# though if it is neither the last or the first, we use it
("0123456789", 1, 7, build_firefox_names("2"), 2),
("0123456789", 2, 7, build_firefox_names("1"), 1),
("0123456789", 8, 7, build_firefox_names("7"), 7),
("0123456789", 7, 7, build_firefox_names("8"), 8),
],
)
def test_approx_index(bdata, mid, around, fnames, result):
# this is always a firefox 64 linux build info
binfo = create_build_info(build_info.IntegrationBuildInfo)

Просмотреть файл

@ -3,16 +3,23 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
from __future__ import absolute_import
import unittest
from mock import patch, Mock, call, MagicMock
import datetime
from mozregression.bisector import (NightlyHandler, IntegrationHandler, Bisector,
Bisection, BisectorHandler)
from mozregression import build_range
from mozregression.errors import LauncherError, MozRegressionError
import datetime
import unittest
from mock import MagicMock, Mock, call, patch
from six.moves import range
from mozregression import build_range
from mozregression.bisector import (
Bisection,
Bisector,
BisectorHandler,
IntegrationHandler,
NightlyHandler,
)
from mozregression.errors import LauncherError, MozRegressionError
class MockBisectorHandler(BisectorHandler):
def _print_progress(self, new_data):
@ -22,41 +29,38 @@ class MockBisectorHandler(BisectorHandler):
class TestBisectorHandler(unittest.TestCase):
def setUp(self):
self.handler = MockBisectorHandler()
self.handler.set_build_range([
{'build_url': 'http://build_url_0', 'repository': 'my'}
])
self.handler.set_build_range([{"build_url": "http://build_url_0", "repository": "my"}])
def test_initialize(self):
self.handler.set_build_range([
Mock(changeset='1', repo_url='my'),
Mock(),
Mock(changeset='3', repo_url='my'),
])
self.handler.set_build_range(
[Mock(changeset="1", repo_url="my"), Mock(), Mock(changeset="3", repo_url="my")]
)
self.handler.initialize()
self.assertEqual(self.handler.found_repo, 'my')
self.assertEqual(self.handler.good_revision, '1')
self.assertEqual(self.handler.bad_revision, '3')
self.assertEqual(self.handler.found_repo, "my")
self.assertEqual(self.handler.good_revision, "1")
self.assertEqual(self.handler.bad_revision, "3")
def test_get_pushlog_url(self):
self.handler.found_repo = 'https://hg.mozilla.repo'
self.handler.good_revision = '2'
self.handler.bad_revision = '6'
self.handler.found_repo = "https://hg.mozilla.repo"
self.handler.good_revision = "2"
self.handler.bad_revision = "6"
self.assertEqual(
self.handler.get_pushlog_url(),
"https://hg.mozilla.repo/pushloghtml?fromchange=2&tochange=6")
"https://hg.mozilla.repo/pushloghtml?fromchange=2&tochange=6",
)
def test_get_pushlog_url_same_chsets(self):
self.handler.found_repo = 'https://hg.mozilla.repo'
self.handler.good_revision = self.handler.bad_revision = '2'
self.handler.found_repo = "https://hg.mozilla.repo"
self.handler.good_revision = self.handler.bad_revision = "2"
self.assertEqual(
self.handler.get_pushlog_url(),
"https://hg.mozilla.repo/pushloghtml?changeset=2")
self.handler.get_pushlog_url(), "https://hg.mozilla.repo/pushloghtml?changeset=2",
)
@patch('mozregression.bisector.LOG')
@patch("mozregression.bisector.LOG")
def test_print_range(self, logger):
self.handler.found_repo = 'https://hg.mozilla.repo'
self.handler.good_revision = '2'
self.handler.bad_revision = '6'
self.handler.found_repo = "https://hg.mozilla.repo"
self.handler.good_revision = "2"
self.handler.bad_revision = "6"
log = []
logger.info = log.append
@ -65,31 +69,28 @@ class TestBisectorHandler(unittest.TestCase):
self.assertEqual(log[1], "First bad revision: 6")
self.assertIn(self.handler.get_pushlog_url(), log[2])
@patch('tests.unit.test_bisector.MockBisectorHandler._print_progress')
@patch("tests.unit.test_bisector.MockBisectorHandler._print_progress")
def test_build_good(self, _print_progress):
self.handler.build_good(0, [{"changeset": '123'},
{"changeset": '456'}])
_print_progress.assert_called_with([{"changeset": '123'},
{"changeset": '456'}])
self.handler.build_good(0, [{"changeset": "123"}, {"changeset": "456"}])
_print_progress.assert_called_with([{"changeset": "123"}, {"changeset": "456"}])
@patch('tests.unit.test_bisector.MockBisectorHandler._print_progress')
@patch("tests.unit.test_bisector.MockBisectorHandler._print_progress")
def test_build_bad(self, _print_progress):
# with at least two, _print_progress will be called
self.handler.build_bad(0, [{"changeset": '123'}, {"changeset": '456'}])
_print_progress.assert_called_with([{"changeset": '123'},
{"changeset": '456'}])
self.handler.build_bad(0, [{"changeset": "123"}, {"changeset": "456"}])
_print_progress.assert_called_with([{"changeset": "123"}, {"changeset": "456"}])
class TestNightlyHandler(unittest.TestCase):
def setUp(self):
self.handler = NightlyHandler()
@patch('mozregression.bisector.BisectorHandler.initialize')
@patch("mozregression.bisector.BisectorHandler.initialize")
def test_initialize(self, initialize):
def get_associated_data(index):
return index
self.handler.build_range = [Mock(build_date=0),
Mock(build_date=1)]
self.handler.build_range = [Mock(build_date=0), Mock(build_date=1)]
self.handler.initialize()
# check that members are set
self.assertEqual(self.handler.good_date, 0)
@ -97,7 +98,7 @@ class TestNightlyHandler(unittest.TestCase):
initialize.assert_called_with(self.handler)
@patch('mozregression.bisector.LOG')
@patch("mozregression.bisector.LOG")
def test_print_progress(self, logger):
log = []
logger.info = log.append
@ -106,25 +107,25 @@ class TestNightlyHandler(unittest.TestCase):
new_data = [
Mock(build_date=datetime.date(2014, 11, 15)),
Mock(build_date=datetime.date(2014, 11, 20))
Mock(build_date=datetime.date(2014, 11, 20)),
]
self.handler._print_progress(new_data)
self.assertIn('from [2014-11-10, 2014-11-20] (10 days)', log[0])
self.assertIn('to [2014-11-15, 2014-11-20] (5 days)', log[0])
self.assertIn('2 steps left', log[0])
self.assertIn("from [2014-11-10, 2014-11-20] (10 days)", log[0])
self.assertIn("to [2014-11-15, 2014-11-20] (5 days)", log[0])
self.assertIn("2 steps left", log[0])
@patch('mozregression.bisector.LOG')
@patch("mozregression.bisector.LOG")
def test_user_exit(self, logger):
log = []
logger.info = log.append
self.handler.good_date = datetime.date(2014, 11, 10)
self.handler.bad_date = datetime.date(2014, 11, 20)
self.handler.user_exit(0)
self.assertEqual('Newest known good nightly: 2014-11-10', log[0])
self.assertEqual('Oldest known bad nightly: 2014-11-20', log[1])
self.assertEqual("Newest known good nightly: 2014-11-10", log[0])
self.assertEqual("Oldest known bad nightly: 2014-11-20", log[1])
@patch('mozregression.bisector.LOG')
@patch("mozregression.bisector.LOG")
def test_print_range_without_repo(self, logger):
log = []
logger.info = log.append
@ -133,14 +134,14 @@ class TestNightlyHandler(unittest.TestCase):
self.handler.bad_date = datetime.date(2014, 11, 20)
self.handler.print_range()
self.assertIn("no pushlog url available", log[0])
self.assertEqual('Newest known good nightly: 2014-11-10', log[1])
self.assertEqual('Oldest known bad nightly: 2014-11-20', log[2])
self.assertEqual("Newest known good nightly: 2014-11-10", log[1])
self.assertEqual("Oldest known bad nightly: 2014-11-20", log[2])
@patch('mozregression.bisector.LOG')
@patch("mozregression.bisector.LOG")
def test_print_range_rev_availables(self, logger):
self.handler.found_repo = 'https://hg.mozilla.repo'
self.handler.good_revision = '2'
self.handler.bad_revision = '6'
self.handler.found_repo = "https://hg.mozilla.repo"
self.handler.good_revision = "2"
self.handler.bad_revision = "6"
self.handler.good_date = datetime.date(2015, 1, 1)
self.handler.bad_date = datetime.date(2015, 1, 2)
log = []
@ -151,89 +152,81 @@ class TestNightlyHandler(unittest.TestCase):
self.assertEqual(log[1], "First bad revision: 6 (2015-01-02)")
self.assertIn(self.handler.get_pushlog_url(), log[2])
@patch('mozregression.bisector.LOG')
@patch("mozregression.bisector.LOG")
def test_print_range_no_rev_availables(self, logger):
self.handler.found_repo = 'https://hg.mozilla.repo'
self.handler.found_repo = "https://hg.mozilla.repo"
self.handler.good_date = datetime.date(2014, 11, 10)
self.handler.bad_date = datetime.date(2014, 11, 20)
log = []
logger.info = log.append
self.handler.print_range()
self.assertEqual('Newest known good nightly: 2014-11-10', log[0])
self.assertEqual('Oldest known bad nightly: 2014-11-20', log[1])
self.assertIn("pushloghtml?startdate=2014-11-10&enddate=2014-11-20",
log[2])
self.assertEqual("Newest known good nightly: 2014-11-10", log[0])
self.assertEqual("Oldest known bad nightly: 2014-11-20", log[1])
self.assertIn("pushloghtml?startdate=2014-11-10&enddate=2014-11-20", log[2])
class TestIntegrationHandler(unittest.TestCase):
def setUp(self):
self.handler = IntegrationHandler()
@patch('mozregression.bisector.LOG')
@patch("mozregression.bisector.LOG")
def test_print_progress(self, logger):
log = []
logger.info = log.append
self.handler.set_build_range([
Mock(short_changeset='12'),
Mock(short_changeset='123'),
Mock(short_changeset='1234'),
Mock(short_changeset='12345'),
])
new_data = [
Mock(short_changeset='1234'),
Mock(short_changeset='12345')
]
self.handler.set_build_range(
[
Mock(short_changeset="12"),
Mock(short_changeset="123"),
Mock(short_changeset="1234"),
Mock(short_changeset="12345"),
]
)
new_data = [Mock(short_changeset="1234"), Mock(short_changeset="12345")]
self.handler._print_progress(new_data)
self.assertIn('from [12, 12345] (4 builds)', log[0])
self.assertIn('to [1234, 12345] (2 builds)', log[0])
self.assertIn('1 steps left', log[0])
self.assertIn("from [12, 12345] (4 builds)", log[0])
self.assertIn("to [1234, 12345] (2 builds)", log[0])
self.assertIn("1 steps left", log[0])
@patch('mozregression.bisector.LOG')
@patch("mozregression.bisector.LOG")
def test_user_exit(self, logger):
log = []
logger.info = log.append
self.handler.good_revision = '3'
self.handler.bad_revision = '1'
self.handler.good_revision = "3"
self.handler.bad_revision = "1"
self.handler.user_exit(0)
self.assertEqual('Newest known good integration revision: 3', log[0])
self.assertEqual('Oldest known bad integration revision: 1', log[1])
self.assertEqual("Newest known good integration revision: 3", log[0])
self.assertEqual("Oldest known bad integration revision: 1", log[1])
class MyBuildData(build_range.BuildRange):
def __init__(self, data=()):
class FutureBuildInfo(build_range.FutureBuildInfo):
def __init__(self, *a, **kwa):
build_range.FutureBuildInfo.__init__(self, *a, **kwa)
self._build_info = Mock(data=self.data)
build_range.BuildRange.__init__(
self,
None,
[FutureBuildInfo(None, v) for v in data]
)
build_range.BuildRange.__init__(self, None, [FutureBuildInfo(None, v) for v in data])
def __repr__(self):
return repr([s.build_info.data for s in self._future_build_infos])
def __eq__(self, other):
return [s.build_info.data for s in self._future_build_infos] == \
[s.build_info.data for s in other._future_build_infos]
return [s.build_info.data for s in self._future_build_infos] == [
s.build_info.data for s in other._future_build_infos
]
class TestBisector(unittest.TestCase):
def setUp(self):
self.handler = MagicMock(find_fix=False, ensure_good_and_bad=False)
self.test_runner = Mock()
self.bisector = Bisector(Mock(), self.test_runner,
Mock(),
dl_in_background=False)
self.bisector = Bisector(Mock(), self.test_runner, Mock(), dl_in_background=False)
self.bisector.download_background = False
# shim for py2.7
if not hasattr(self, 'assertRaisesRegex'):
if not hasattr(self, "assertRaisesRegex"):
self.assertRaisesRegex = self.assertRaisesRegexp
def test__bisect_no_data(self):
@ -262,214 +255,216 @@ class TestBisector(unittest.TestCase):
if isinstance(verdict, Exception):
raise verdict
return verdict
self.test_runner.evaluate = Mock(side_effect=evaluate)
result = self.bisector._bisect(self.handler, build_range)
return {
'result': result,
"result": result,
}
def test_ensure_good_bad_invalid(self):
self.handler.ensure_good_and_bad = True
with self.assertRaisesRegex(MozRegressionError,
"expected to be good"):
self.do__bisect(MyBuildData([1, 2, 3, 4, 5]), ['b'])
with self.assertRaisesRegex(MozRegressionError, "expected to be good"):
self.do__bisect(MyBuildData([1, 2, 3, 4, 5]), ["b"])
with self.assertRaisesRegex(MozRegressionError,
"expected to be bad"):
self.do__bisect(MyBuildData([1, 2, 3, 4, 5]), ['g', 'g'])
with self.assertRaisesRegex(MozRegressionError, "expected to be bad"):
self.do__bisect(MyBuildData([1, 2, 3, 4, 5]), ["g", "g"])
def test_ensure_good_bad(self):
self.handler.ensure_good_and_bad = True
data = MyBuildData([1, 2, 3, 4, 5])
self.do__bisect(data,
['s', 'r', 'g', 'b', 'e'])
self.test_runner.evaluate.assert_has_calls([
call(data[0]), # tested good (then skip)
call(data[0]), # tested good (then retry)
call(data[0]), # tested good
call(data[-1]), # tested bad
])
self.assertEqual(
self.bisector.download_manager.download_in_background.call_count,
0
self.do__bisect(data, ["s", "r", "g", "b", "e"])
self.test_runner.evaluate.assert_has_calls(
[
call(data[0]), # tested good (then skip)
call(data[0]), # tested good (then retry)
call(data[0]), # tested good
call(data[-1]), # tested bad
]
)
self.assertEqual(self.bisector.download_manager.download_in_background.call_count, 0)
def test_ensure_good_bad_with_bg_dl(self):
self.handler.ensure_good_and_bad = True
self.bisector.dl_in_background = True
data = MyBuildData([1, 2, 3, 4, 5])
self.do__bisect(data,
['s', 'r', 'g', 'e'])
self.test_runner.evaluate.assert_has_calls([
call(data[0]), # tested good (then skip)
call(data[0]), # tested good (then retry)
call(data[0]), # tested good
call(data[-1]), # tested bad
])
self.do__bisect(data, ["s", "r", "g", "e"])
self.test_runner.evaluate.assert_has_calls(
[
call(data[0]), # tested good (then skip)
call(data[0]), # tested good (then retry)
call(data[0]), # tested good
call(data[-1]), # tested bad
]
)
self.bisector.download_manager.download_in_background.assert_has_calls(
[call(data[-1]), # bad in backgound
call(data[data.mid_point()])] # and mid build
[call(data[-1]), call(data[data.mid_point()])] # bad in backgound # and mid build
)
def test_ensure_good_bad_with_find_fix(self):
self.handler.ensure_good_and_bad = True
self.handler.find_fix = True
data = MyBuildData([1, 2, 3, 4, 5])
self.do__bisect(data, ['g', 'e'])
self.test_runner.evaluate.assert_has_calls([
call(data[-1]), # tested good (then skip)
call(data[0]), # tested bad
])
self.do__bisect(data, ["g", "e"])
self.test_runner.evaluate.assert_has_calls(
[call(data[-1]), call(data[0])] # tested good (then skip) # tested bad
)
def test__bisect_case1(self):
test_result = self.do__bisect(MyBuildData([1, 2, 3, 4, 5]), ['g', 'b'])
test_result = self.do__bisect(MyBuildData([1, 2, 3, 4, 5]), ["g", "b"])
# check that set_build_range was called
self.handler.set_build_range.assert_has_calls([
# first call
call(MyBuildData([1, 2, 3, 4, 5])),
# we answered good
call(MyBuildData([3, 4, 5])),
# we answered bad
call(MyBuildData([3, 4])),
])
self.handler.set_build_range.assert_has_calls(
[
# first call
call(MyBuildData([1, 2, 3, 4, 5])),
# we answered good
call(MyBuildData([3, 4, 5])),
# we answered bad
call(MyBuildData([3, 4])),
]
)
# ensure that we called the handler's methods
self.handler.initialize.assert_called_with()
self.handler.build_good.assert_called_with(2, MyBuildData([3, 4, 5]))
self.handler.build_bad.assert_called_with(1, MyBuildData([3, 4]))
self.assertTrue(self.handler.build_range.ensure_limits_called)
# bisection is finished
self.assertEqual(test_result['result'], Bisection.FINISHED)
self.assertEqual(test_result["result"], Bisection.FINISHED)
def test__bisect_with_launcher_exception(self):
test_result = self.do__bisect(MyBuildData([1, 2, 3, 4, 5]),
['g', LauncherError("err")])
test_result = self.do__bisect(MyBuildData([1, 2, 3, 4, 5]), ["g", LauncherError("err")])
# check that set_build_range was called
self.handler.set_build_range.assert_has_calls([
# first call
call(MyBuildData([1, 2, 3, 4, 5])),
# we answered good
call(MyBuildData([3, 4, 5])),
# launcher exception, equivalent to a skip
call(MyBuildData([3, 5])),
])
self.handler.set_build_range.assert_has_calls(
[
# first call
call(MyBuildData([1, 2, 3, 4, 5])),
# we answered good
call(MyBuildData([3, 4, 5])),
# launcher exception, equivalent to a skip
call(MyBuildData([3, 5])),
]
)
# ensure that we called the handler's methods
self.handler.initialize.assert_called_with()
self.handler.build_good.assert_called_with(2, MyBuildData([3, 4, 5]))
self.handler.build_skip.assert_called_with(1)
self.assertTrue(self.handler.build_range.ensure_limits_called)
# bisection is finished
self.assertEqual(test_result['result'], Bisection.FINISHED)
self.assertEqual(test_result["result"], Bisection.FINISHED)
def test__bisect_case1_hunt_fix(self):
self.handler.find_fix = True
test_result = self.do__bisect(MyBuildData([1, 2, 3, 4, 5]), ['g', 'b'])
test_result = self.do__bisect(MyBuildData([1, 2, 3, 4, 5]), ["g", "b"])
# check that set_build_range was called
self.handler.set_build_range.assert_has_calls([
# first call
call(MyBuildData([1, 2, 3, 4, 5])),
# we answered good
call(MyBuildData([1, 2, 3])),
# we answered bad
call(MyBuildData([2, 3])),
])
self.handler.set_build_range.assert_has_calls(
[
# first call
call(MyBuildData([1, 2, 3, 4, 5])),
# we answered good
call(MyBuildData([1, 2, 3])),
# we answered bad
call(MyBuildData([2, 3])),
]
)
# ensure that we called the handler's methods
self.assertEqual(self.handler.initialize.mock_calls, [call()] * 3)
self.handler.build_good. \
assert_called_once_with(2, MyBuildData([1, 2, 3]))
self.handler.build_good.assert_called_once_with(2, MyBuildData([1, 2, 3]))
self.handler.build_bad.assert_called_once_with(1, MyBuildData([2, 3]))
# bisection is finished
self.assertEqual(test_result['result'], Bisection.FINISHED)
self.assertEqual(test_result["result"], Bisection.FINISHED)
def test__bisect_case2(self):
test_result = self.do__bisect(MyBuildData([1, 2, 3]), ['r', 's'])
test_result = self.do__bisect(MyBuildData([1, 2, 3]), ["r", "s"])
# check that set_build_range was called
self.handler.set_build_range.assert_has_calls([
# first call
call(MyBuildData([1, 2, 3])),
# we asked for a retry
call(MyBuildData([1, 2, 3])),
# we skipped one
call(MyBuildData([1, 3])),
])
self.handler.set_build_range.assert_has_calls(
[
# first call
call(MyBuildData([1, 2, 3])),
# we asked for a retry
call(MyBuildData([1, 2, 3])),
# we skipped one
call(MyBuildData([1, 3])),
]
)
# ensure that we called the handler's methods
self.handler.initialize.assert_called_with()
self.handler.build_retry.assert_called_with(1)
self.handler.build_skip.assert_called_with(1)
self.assertTrue(self.handler.build_range.ensure_limits_called)
# bisection is finished
self.assertEqual(test_result['result'], Bisection.FINISHED)
self.assertEqual(test_result["result"], Bisection.FINISHED)
def test__bisect_with_back(self):
test_result = self.do__bisect(MyBuildData([1, 2, 3, 4, 5]),
['g', 'back', 'b', 'g'])
test_result = self.do__bisect(MyBuildData([1, 2, 3, 4, 5]), ["g", "back", "b", "g"])
# check that set_build_range was called
self.handler.set_build_range.assert_has_calls([
# first call
call(MyBuildData([1, 2, 3, 4, 5])),
# we answered good
call(MyBuildData([3, 4, 5])),
# oups! let's go back
call(MyBuildData([1, 2, 3, 4, 5])),
# we answered bad this time
call(MyBuildData([1, 2, 3])),
# then good
call(MyBuildData([2, 3])),
])
self.handler.set_build_range.assert_has_calls(
[
# first call
call(MyBuildData([1, 2, 3, 4, 5])),
# we answered good
call(MyBuildData([3, 4, 5])),
# oups! let's go back
call(MyBuildData([1, 2, 3, 4, 5])),
# we answered bad this time
call(MyBuildData([1, 2, 3])),
# then good
call(MyBuildData([2, 3])),
]
)
# bisection is finished
self.assertEqual(test_result['result'], Bisection.FINISHED)
self.assertEqual(test_result["result"], Bisection.FINISHED)
def test__bisect_user_exit(self):
test_result = self.do__bisect(MyBuildData(list(range(20))), ['e'])
test_result = self.do__bisect(MyBuildData(list(range(20))), ["e"])
# check that set_build_range was called
self.handler.set_build_range.\
assert_has_calls([call(MyBuildData(list(range(20))))])
self.handler.set_build_range.assert_has_calls([call(MyBuildData(list(range(20))))])
# ensure that we called the handler's method
self.handler.initialize.assert_called_once_with()
self.handler.user_exit.assert_called_with(10)
# user exit
self.assertEqual(test_result['result'], Bisection.USER_EXIT)
self.assertEqual(test_result["result"], Bisection.USER_EXIT)
def test__bisect_with_background_download(self):
self.bisector.dl_in_background = True
test_result = self.do__bisect(MyBuildData([1, 2, 3, 4, 5]), ['g', 'b'])
test_result = self.do__bisect(MyBuildData([1, 2, 3, 4, 5]), ["g", "b"])
# check that set_build_range was called
self.handler.set_build_range.assert_has_calls([
call(MyBuildData([1, 2, 3, 4, 5])), # first call
call(MyBuildData([3, 4, 5])), # we answered good
call(MyBuildData([3, 4])) # we answered bad
])
self.handler.set_build_range.assert_has_calls(
[
call(MyBuildData([1, 2, 3, 4, 5])), # first call
call(MyBuildData([3, 4, 5])), # we answered good
call(MyBuildData([3, 4])), # we answered bad
]
)
# ensure that we called the handler's methods
self.handler.initialize.assert_called_with()
self.handler.build_good.assert_called_with(2, MyBuildData([3, 4, 5]))
self.handler.build_bad.assert_called_with(1, MyBuildData([3, 4]))
self.assertTrue(self.handler.build_range.ensure_limits_called)
# bisection is finished
self.assertEqual(test_result['result'], Bisection.FINISHED)
self.assertEqual(test_result["result"], Bisection.FINISHED)
@patch('mozregression.bisector.Bisector._bisect')
@patch("mozregression.bisector.Bisector._bisect")
def test_bisect(self, _bisect):
_bisect.return_value = 1
build_range = Mock()
create_range = Mock(return_value=build_range)
self.handler.create_range = create_range
result = self.bisector.bisect(self.handler, 'g', 'b', s=1)
create_range.assert_called_with(self.bisector.fetch_config,
'g', 'b', s=1)
result = self.bisector.bisect(self.handler, "g", "b", s=1)
create_range.assert_called_with(self.bisector.fetch_config, "g", "b", s=1)
self.assertFalse(build_range.reverse.called)
_bisect.assert_called_with(self.handler, build_range)
self.assertEqual(result, 1)
@patch('mozregression.bisector.Bisector._bisect')
@patch("mozregression.bisector.Bisector._bisect")
def test_bisect_reverse(self, _bisect):
build_range = Mock()
create_range = Mock(return_value=build_range)
self.handler.create_range = create_range
self.handler.find_fix = True
self.bisector.bisect(self.handler, 'g', 'b', s=1)
create_range.assert_called_with(self.bisector.fetch_config,
'b', 'g', s=1)
self.bisector.bisect(self.handler, "g", "b", s=1)
create_range.assert_called_with(self.bisector.fetch_config, "b", "g", s=1)
_bisect.assert_called_with(self.handler, build_range)
if __name__ == '__main__':
if __name__ == "__main__":
unittest.main()

Просмотреть файл

@ -1,37 +1,45 @@
from __future__ import absolute_import
from mozregression import branches, errors
import pytest
from mozregression import branches, errors
@pytest.mark.parametrize('branch,alias', [
("mozilla-inbound", "mozilla-inbound"),
("mozilla-inbound", "m-i"),
("mozilla-central", "mozilla-central"),
("mozilla-central", "m-c"),
("unknown", "unknown")
])
@pytest.mark.parametrize(
"branch,alias",
[
("mozilla-inbound", "mozilla-inbound"),
("mozilla-inbound", "m-i"),
("mozilla-central", "mozilla-central"),
("mozilla-central", "m-c"),
("unknown", "unknown"),
],
)
def test_branch_name(branch, alias):
assert branch == branches.get_name(alias)
@pytest.mark.parametrize('branch,url', [
("m-c",
"https://hg.mozilla.org/mozilla-central"),
("m-i",
"https://hg.mozilla.org/integration/mozilla-inbound"),
("mozilla-beta",
"https://hg.mozilla.org/releases/mozilla-beta")
])
@pytest.mark.parametrize(
"branch,url",
[
("m-c", "https://hg.mozilla.org/mozilla-central"),
("m-i", "https://hg.mozilla.org/integration/mozilla-inbound"),
("mozilla-beta", "https://hg.mozilla.org/releases/mozilla-beta"),
],
)
def test_get_urls(branch, url):
assert branches.get_url(branch) == url
@pytest.mark.parametrize('category,present,not_present', [
# no category, list all branches but not aliases
(None, ['mozilla-central', 'mozilla-inbound'], ['m-c', 'm-i']),
# specific category list only branches under that category
('integration', ['mozilla-inbound'], ['m-i', 'mozilla-central']),
])
@pytest.mark.parametrize(
"category,present,not_present",
[
# no category, list all branches but not aliases
(None, ["mozilla-central", "mozilla-inbound"], ["m-c", "m-i"]),
# specific category list only branches under that category
("integration", ["mozilla-inbound"], ["m-i", "mozilla-central"]),
],
)
def test_get_branches(category, present, not_present):
names = branches.get_branches(category=category)
for name in present:
@ -45,32 +53,32 @@ def test_get_url_unknown_branch():
branches.get_url("unknown branch")
@pytest.mark.parametrize('name, expected', [
('mozilla-central', 'default'),
('autoland', 'integration'),
('m-i', 'integration'),
('release', 'releases'),
('mozilla-beta', 'releases'),
('', None),
(None, None)
])
@pytest.mark.parametrize(
"name, expected",
[
("mozilla-central", "default"),
("autoland", "integration"),
("m-i", "integration"),
("release", "releases"),
("mozilla-beta", "releases"),
("", None),
(None, None),
],
)
def test_get_category(name, expected):
assert branches.get_category(name) == expected
@pytest.mark.parametrize('commit, branch, current', [
("Merge mozilla-central to autoland",
"mozilla-central", "autoland"),
("Merge mozilla-central to autoland",
"autoland", "mozilla-central"),
("Merge autoland to central, a=merge",
"autoland", "mozilla-central"),
("merge autoland to mozilla-central a=merge",
"autoland", "mozilla-central"),
("Merge m-i to m-c, a=merge CLOSED TREE",
"mozilla-inbound", "mozilla-central"),
("Merge mozilla inbound to central a=merge",
"mozilla-inbound", "mozilla-central"),
])
@pytest.mark.parametrize(
"commit, branch, current",
[
("Merge mozilla-central to autoland", "mozilla-central", "autoland"),
("Merge mozilla-central to autoland", "autoland", "mozilla-central"),
("Merge autoland to central, a=merge", "autoland", "mozilla-central"),
("merge autoland to mozilla-central a=merge", "autoland", "mozilla-central"),
("Merge m-i to m-c, a=merge CLOSED TREE", "mozilla-inbound", "mozilla-central"),
("Merge mozilla inbound to central a=merge", "mozilla-inbound", "mozilla-central",),
],
)
def test_find_branch_in_merge_commit(commit, branch, current):
assert branches.find_branch_in_merge_commit(commit, current) == branch

Просмотреть файл

@ -1,18 +1,20 @@
from __future__ import absolute_import
import pytest
from datetime import date, datetime
from mozregression.fetch_configs import create_config
import pytest
from mozregression import build_info
from mozregression.fetch_configs import create_config
def create_build_info(klass, **attrs):
defaults = dict(
fetch_config=create_config('firefox', 'linux', 64, 'x86_64'),
build_url='http://build/url',
fetch_config=create_config("firefox", "linux", 64, "x86_64"),
build_url="http://build/url",
build_date=date(2015, 9, 1),
changeset='12ab' * 10,
repo_url='http://repo:url',
changeset="12ab" * 10,
repo_url="http://repo:url",
)
defaults.update(attrs)
return klass(**defaults)
@ -20,25 +22,24 @@ def create_build_info(klass, **attrs):
def read_only(klass):
defaults = [
('app_name', 'firefox'),
('build_url', 'http://build/url'),
('build_date', date(2015, 9, 1)),
('changeset', '12ab' * 10),
('short_changeset', '12ab12ab'),
('repo_url', 'http://repo:url'),
("app_name", "firefox"),
("build_url", "http://build/url"),
("build_date", date(2015, 9, 1)),
("changeset", "12ab" * 10),
("short_changeset", "12ab12ab"),
("repo_url", "http://repo:url"),
]
if klass is build_info.NightlyBuildInfo:
defaults.extend([('repo_name', 'mozilla-central'),
('build_type', 'nightly')])
defaults.extend([("repo_name", "mozilla-central"), ("build_type", "nightly")])
else:
defaults.extend([('repo_name', 'mozilla-central'),
('build_type', 'integration')])
defaults.extend([("repo_name", "mozilla-central"), ("build_type", "integration")])
return [(klass, attr, value) for attr, value in defaults]
@pytest.mark.parametrize('klass, attr, value',
read_only(build_info.NightlyBuildInfo) +
read_only(build_info.IntegrationBuildInfo))
@pytest.mark.parametrize(
"klass, attr, value",
read_only(build_info.NightlyBuildInfo) + read_only(build_info.IntegrationBuildInfo),
)
def test_read_only_attrs(klass, attr, value):
binfo = create_build_info(klass)
assert getattr(binfo, attr) == value
@ -47,70 +48,68 @@ def test_read_only_attrs(klass, attr, value):
setattr(binfo, attr, value)
@pytest.mark.parametrize('klass, attr, value', [
(build_info.NightlyBuildInfo, 'build_file', '/build/file'),
(build_info.IntegrationBuildInfo, 'build_file', '/build/file'),
])
@pytest.mark.parametrize(
"klass, attr, value",
[
(build_info.NightlyBuildInfo, "build_file", "/build/file"),
(build_info.IntegrationBuildInfo, "build_file", "/build/file"),
],
)
def test_writable_attrs(klass, attr, value):
binfo = create_build_info(klass)
setattr(binfo, attr, value)
assert getattr(binfo, attr) == value
@pytest.mark.parametrize('klass', [
build_info.NightlyBuildInfo,
build_info.IntegrationBuildInfo
])
@pytest.mark.parametrize("klass", [build_info.NightlyBuildInfo, build_info.IntegrationBuildInfo])
def test_update_from_app_info(klass):
app_info = {
'application_changeset': 'chset',
'application_repository': 'repo',
"application_changeset": "chset",
"application_repository": "repo",
}
binfo = create_build_info(klass, changeset=None, repo_url=None)
assert binfo.changeset is None
assert binfo.repo_url is None
binfo.update_from_app_info(app_info)
# binfo updated
assert binfo.changeset == 'chset'
assert binfo.repo_url == 'repo'
assert binfo.changeset == "chset"
assert binfo.repo_url == "repo"
binfo = create_build_info(klass)
# if values were defined, nothing is updated
assert binfo.changeset is not None
assert binfo.repo_url is not None
binfo.update_from_app_info(app_info)
assert binfo.changeset != 'chset'
assert binfo.repo_url != 'repo'
assert binfo.changeset != "chset"
assert binfo.repo_url != "repo"
@pytest.mark.parametrize('klass', [
build_info.NightlyBuildInfo,
build_info.IntegrationBuildInfo
])
@pytest.mark.parametrize("klass", [build_info.NightlyBuildInfo, build_info.IntegrationBuildInfo])
def test_to_dict(klass):
binfo = create_build_info(klass)
dct = binfo.to_dict()
assert isinstance(dct, dict)
assert 'app_name' in dct
assert dct['app_name'] == 'firefox'
assert "app_name" in dct
assert dct["app_name"] == "firefox"
@pytest.mark.parametrize('klass,extra,result', [
# build with defaults given in create_build_info
(build_info.NightlyBuildInfo,
{},
'2015-09-01--mozilla-central--url'),
# this time with a datetime instance (buildid)
(build_info.NightlyBuildInfo,
{'build_date': datetime(2015, 11, 16, 10, 2, 5)},
'2015-11-16-10-02-05--mozilla-central--url'),
# same but for integration
(build_info.IntegrationBuildInfo,
{},
'12ab12ab12ab-shippable--mozilla-central--url'),
])
@pytest.mark.parametrize(
"klass,extra,result",
[
# build with defaults given in create_build_info
(build_info.NightlyBuildInfo, {}, "2015-09-01--mozilla-central--url"),
# this time with a datetime instance (buildid)
(
build_info.NightlyBuildInfo,
{"build_date": datetime(2015, 11, 16, 10, 2, 5)},
"2015-11-16-10-02-05--mozilla-central--url",
),
# same but for integration
(build_info.IntegrationBuildInfo, {}, "12ab12ab12ab-shippable--mozilla-central--url",),
],
)
def test_persist_filename(klass, extra, result):
persist_part = extra.pop('persist_part', None)
persist_part = extra.pop("persist_part", None)
binfo = create_build_info(klass, **extra)
if persist_part:
# fake that the fetch config should return the persist_part

Просмотреть файл

@ -1,32 +1,16 @@
from __future__ import absolute_import
import pytest
from datetime import date, datetime, timedelta
import pytest
from six.moves import range
from mozregression import build_range
from mozregression.fetch_build_info import InfoFetcher
from mozregression.errors import BuildInfoNotFound
from mozregression.fetch_configs import create_config
from mozregression.json_pushes import JsonPushes
from .test_fetch_configs import create_push
from six.moves import range
class RangeCreator(object):
def __init__(self, mocker):
self.mocker = mocker
def create(self, values):
info_fetcher = self.mocker.Mock(spec=InfoFetcher)
info_fetcher.find_build_info.side_effect = lambda i: i
future_build_infos = [
build_range.FutureBuildInfo(info_fetcher, v) for v in values
]
return build_range.BuildRange(info_fetcher, future_build_infos)
@pytest.fixture
def range_creator(mocker):
return RangeCreator(mocker)
def test_len(range_creator):
@ -37,8 +21,7 @@ def test_access(range_creator):
build_range = range_creator.create(list(range(5)))
assert build_range[0] == 0
build_range.build_info_fetcher.find_build_info.side_effect = \
BuildInfoNotFound
build_range.build_info_fetcher.find_build_info.side_effect = BuildInfoNotFound
assert build_range[1] is False
# even if one build is invalid, item access do not modify the range
@ -69,11 +52,11 @@ def test_deleted(range_creator):
def fetch_unless(br, func):
def fetch(index):
if func(index):
raise BuildInfoNotFound("")
return index
br.build_info_fetcher.find_build_info.side_effect = fetch
@ -98,8 +81,8 @@ def test_mid_point_interrupt(range_creator):
def _build_range(fb, rng):
return build_range.BuildRange(
fb.build_info_fetcher,
[build_range.FutureBuildInfo(fb.build_info_fetcher, i) for i in rng])
fb.build_info_fetcher, [build_range.FutureBuildInfo(fb.build_info_fetcher, i) for i in rng],
)
def range_before(fb, expand):
@ -110,37 +93,53 @@ def range_after(fb, expand):
return _build_range(fb, list(range(fb.data + 1, fb.data + 1 + expand)))
@pytest.mark.parametrize('size_expand,initial,fail_in,expected,error', [
# short range
(10, list(range(1)), [], list(range(1)), None),
# empty range after removing invalids
(10, list(range(2)), [0, 1], [], None),
# lower limit missing
(10, list(range(10)), [0], [-1] + list(range(1, 10)), None),
# higher limit missing
(10, list(range(10)), [9], list(range(0, 9)) + [10], None),
# lower and higher limit missing
(10, list(range(10)), [0, 9], [-1] + list(range(1, 9)) + [10], None),
# lower missing, with missing builds in the before range
(10, list(range(10)), list(range(-5, 1)), [-6] + list(range(1, 10)), None),
# higher missing, with missing builds in the after range
(10, list(range(10)), list(range(9, 15)), list(range(0, 9)) + [15], None),
# lower and higher missing, with missing builds in the before/after range
(10, list(range(10)), list(range(-6, 1)) + list(range(9, 14)), [-7] + list(range(1, 9)) + [14],
None),
# unable to find any valid builds in before range
(10, list(range(10)), list(range(-10, 1)), list(range(1, 10)),
["can't find a build before"]),
# unable to find any valid builds in after range
(10, list(range(10)), list(range(9, 20)), list(range(0, 9)),
["can't find a build after"]),
# unable to find valid builds in before and after
(10, list(range(10)), list(range(-10, 1)) + list(range(9, 20)), list(range(1, 9)),
["can't find a build before", "can't find a build after"]),
])
def test_check_expand(mocker, range_creator, size_expand, initial, fail_in,
expected, error):
log = mocker.patch('mozregression.build_range.LOG')
@pytest.mark.parametrize(
"size_expand,initial,fail_in,expected,error",
[
# short range
(10, list(range(1)), [], list(range(1)), None),
# empty range after removing invalids
(10, list(range(2)), [0, 1], [], None),
# lower limit missing
(10, list(range(10)), [0], [-1] + list(range(1, 10)), None),
# higher limit missing
(10, list(range(10)), [9], list(range(0, 9)) + [10], None),
# lower and higher limit missing
(10, list(range(10)), [0, 9], [-1] + list(range(1, 9)) + [10], None),
# lower missing, with missing builds in the before range
(10, list(range(10)), list(range(-5, 1)), [-6] + list(range(1, 10)), None),
# higher missing, with missing builds in the after range
(10, list(range(10)), list(range(9, 15)), list(range(0, 9)) + [15], None),
# lower and higher missing, with missing builds in the before/after range
(
10,
list(range(10)),
list(range(-6, 1)) + list(range(9, 14)),
[-7] + list(range(1, 9)) + [14],
None,
),
# unable to find any valid builds in before range
(
10,
list(range(10)),
list(range(-10, 1)),
list(range(1, 10)),
["can't find a build before"],
),
# unable to find any valid builds in after range
(10, list(range(10)), list(range(9, 20)), list(range(0, 9)), ["can't find a build after"],),
# unable to find valid builds in before and after
(
10,
list(range(10)),
list(range(-10, 1)) + list(range(9, 20)),
list(range(1, 9)),
["can't find a build before", "can't find a build after"],
),
],
)
def test_check_expand(mocker, range_creator, size_expand, initial, fail_in, expected, error):
log = mocker.patch("mozregression.build_range.LOG")
build_range = range_creator.create(initial)
fetch_unless(build_range, lambda i: i in fail_in)
@ -161,8 +160,7 @@ def test_check_expand_interrupt(range_creator):
build_range.mid_point = lambda **kwa: mp() # do not interrupt in there
with pytest.raises(StopIteration):
build_range.check_expand(5, range_before, range_after,
interrupt=lambda: True)
build_range.check_expand(5, range_before, range_after, interrupt=lambda: True)
def test_index(range_creator):
@ -177,19 +175,16 @@ def test_index(range_creator):
def test_get_integration_range(mocker):
fetch_config = create_config('firefox', 'linux', 64, 'x86_64')
jpush_class = mocker.patch('mozregression.fetch_build_info.JsonPushes')
pushes = [create_push('b', 1), create_push('d', 2), create_push('f', 3)]
jpush = mocker.Mock(
pushes_within_changes=mocker.Mock(return_value=pushes),
spec=JsonPushes
)
fetch_config = create_config("firefox", "linux", 64, "x86_64")
jpush_class = mocker.patch("mozregression.fetch_build_info.JsonPushes")
pushes = [create_push("b", 1), create_push("d", 2), create_push("f", 3)]
jpush = mocker.Mock(pushes_within_changes=mocker.Mock(return_value=pushes), spec=JsonPushes)
jpush_class.return_value = jpush
b_range = build_range.get_integration_range(fetch_config, 'a', 'e')
b_range = build_range.get_integration_range(fetch_config, "a", "e")
jpush_class.assert_called_once_with(branch='mozilla-central')
jpush.pushes_within_changes.assert_called_once_with('a', 'e')
jpush_class.assert_called_once_with(branch="mozilla-central")
jpush.pushes_within_changes.assert_called_once_with("a", "e")
assert isinstance(b_range, build_range.BuildRange)
assert len(b_range) == 3
@ -198,27 +193,23 @@ def test_get_integration_range(mocker):
assert b_range[1] == pushes[1]
assert b_range[2] == pushes[2]
b_range.future_build_infos[0].date_or_changeset() == 'b'
b_range.future_build_infos[0].date_or_changeset() == "b"
def test_get_integration_range_with_expand(mocker):
fetch_config = create_config('firefox', 'linux', 64, 'x86_64')
jpush_class = mocker.patch('mozregression.fetch_build_info.JsonPushes')
pushes = [create_push('b', 1), create_push('d', 2), create_push('f', 3)]
jpush = mocker.Mock(
pushes_within_changes=mocker.Mock(return_value=pushes),
spec=JsonPushes
)
fetch_config = create_config("firefox", "linux", 64, "x86_64")
jpush_class = mocker.patch("mozregression.fetch_build_info.JsonPushes")
pushes = [create_push("b", 1), create_push("d", 2), create_push("f", 3)]
jpush = mocker.Mock(pushes_within_changes=mocker.Mock(return_value=pushes), spec=JsonPushes)
jpush_class.return_value = jpush
check_expand = mocker.patch(
'mozregression.build_range.BuildRange.check_expand')
check_expand = mocker.patch("mozregression.build_range.BuildRange.check_expand")
build_range.get_integration_range(fetch_config, 'a', 'e', expand=10)
build_range.get_integration_range(fetch_config, "a", "e", expand=10)
check_expand.assert_called_once_with(
10, build_range.tc_range_before, build_range.tc_range_after,
interrupt=None)
10, build_range.tc_range_before, build_range.tc_range_after, interrupt=None
)
DATE_NOW = datetime.now()
@ -227,34 +218,32 @@ DATE_YEAR_BEFORE = DATE_NOW + timedelta(days=-365)
DATE_TOO_OLD = DATE_YEAR_BEFORE + timedelta(days=-5)
@pytest.mark.parametrize('start_date,end_date,start_call,end_call', [
(DATE_BEFORE_NOW, DATE_NOW, DATE_BEFORE_NOW, DATE_NOW),
# if a date is older than last year, it won't be honored
(DATE_TOO_OLD, DATE_NOW, DATE_YEAR_BEFORE, DATE_NOW),
])
def test_get_integration_range_with_dates(mocker, start_date, end_date,
start_call, end_call):
fetch_config = create_config('firefox', 'linux', 64, 'x86_64')
jpush_class = mocker.patch('mozregression.fetch_build_info.JsonPushes')
jpush = mocker.Mock(
pushes_within_changes=mocker.Mock(return_value=[]),
spec=JsonPushes
)
@pytest.mark.parametrize(
"start_date,end_date,start_call,end_call",
[
(DATE_BEFORE_NOW, DATE_NOW, DATE_BEFORE_NOW, DATE_NOW),
# if a date is older than last year, it won't be honored
(DATE_TOO_OLD, DATE_NOW, DATE_YEAR_BEFORE, DATE_NOW),
],
)
def test_get_integration_range_with_dates(mocker, start_date, end_date, start_call, end_call):
fetch_config = create_config("firefox", "linux", 64, "x86_64")
jpush_class = mocker.patch("mozregression.fetch_build_info.JsonPushes")
jpush = mocker.Mock(pushes_within_changes=mocker.Mock(return_value=[]), spec=JsonPushes)
jpush_class.return_value = jpush
build_range.get_integration_range(fetch_config, start_date, end_date,
time_limit=DATE_YEAR_BEFORE)
build_range.get_integration_range(
fetch_config, start_date, end_date, time_limit=DATE_YEAR_BEFORE
)
jpush.pushes_within_changes.assert_called_once_with(start_call, end_call)
def test_get_nightly_range():
fetch_config = create_config('firefox', 'linux', 64, 'x86_64')
fetch_config = create_config("firefox", "linux", 64, "x86_64")
b_range = build_range.get_nightly_range(
fetch_config,
date(2015, 0o1, 0o1),
date(2015, 0o1, 0o3)
fetch_config, date(2015, 0o1, 0o1), date(2015, 0o1, 0o3)
)
assert isinstance(b_range, build_range.BuildRange)
@ -266,14 +255,16 @@ def test_get_nightly_range():
assert b_range[2] == date(2015, 0o1, 0o3)
@pytest.mark.parametrize('start,end,range_size', [
(datetime(2015, 11, 16, 10, 2, 5), date(2015, 11, 19), 4),
(date(2015, 11, 14), datetime(2015, 11, 16, 10, 2, 5), 3),
(datetime(2015, 11, 16, 10, 2, 5),
datetime(2015, 11, 20, 11, 4, 9), 5),
])
@pytest.mark.parametrize(
"start,end,range_size",
[
(datetime(2015, 11, 16, 10, 2, 5), date(2015, 11, 19), 4),
(date(2015, 11, 14), datetime(2015, 11, 16, 10, 2, 5), 3),
(datetime(2015, 11, 16, 10, 2, 5), datetime(2015, 11, 20, 11, 4, 9), 5),
],
)
def test_get_nightly_range_datetime(start, end, range_size):
fetch_config = create_config('firefox', 'linux', 64, 'x86_64')
fetch_config = create_config("firefox", "linux", 64, "x86_64")
b_range = build_range.get_nightly_range(fetch_config, start, end)
@ -288,13 +279,15 @@ def test_get_nightly_range_datetime(start, end, range_size):
assert isinstance(b_range[i], date)
@pytest.mark.parametrize('func,start,size,expected_range', [
(build_range.tc_range_before, 1, 2, [-1, 0]),
(build_range.tc_range_after, 1, 3, list(range(2, 5))),
])
@pytest.mark.parametrize(
"func,start,size,expected_range",
[
(build_range.tc_range_before, 1, 2, [-1, 0]),
(build_range.tc_range_after, 1, 3, list(range(2, 5))),
],
)
def test_tc_range_before_after(mocker, func, start, size, expected_range):
ftc = build_range.FutureBuildInfo(mocker.Mock(),
mocker.Mock(push_id=start))
ftc = build_range.FutureBuildInfo(mocker.Mock(), mocker.Mock(push_id=start))
def pushes(startID, endID):
# startID: greaterThan - endID: up to and including

Просмотреть файл

@ -1,27 +1,26 @@
from __future__ import absolute_import
from mozregression.class_registry import ClassRegistry
TEST_REGISTRY = ClassRegistry()
@TEST_REGISTRY.register('c1')
@TEST_REGISTRY.register("c1")
class C1(object):
pass
@TEST_REGISTRY.register('c2')
@TEST_REGISTRY.register("c2")
class C2(object):
pass
@TEST_REGISTRY.register('c3', some_other_attr=True)
@TEST_REGISTRY.register("c3", some_other_attr=True)
class C3(object):
pass
def test_get_names():
TEST_REGISTRY.names() == ['c1', 'c2', 'c3']
TEST_REGISTRY.names() == ["c1", "c2", "c3"]
TEST_REGISTRY.names(
lambda klass: getattr(klass, 'some_other_attr', None)
) == ['c3']
TEST_REGISTRY.names(lambda klass: getattr(klass, "some_other_attr", None)) == ["c3"]

Просмотреть файл

@ -3,19 +3,20 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
from __future__ import absolute_import
import unittest
import tempfile
import os
import datetime
import pytest
import datetime
import os
import tempfile
import unittest
import pytest
import six
from mock import patch
from mozlog import get_default_logger
from mozregression import cli, errors
from mozregression.releases import releases
from mozregression.fetch_configs import create_config
import six
from mozregression.releases import releases
class TestParseDate(unittest.TestCase):
@ -28,25 +29,23 @@ class TestParseDate(unittest.TestCase):
self.assertEqual(date, datetime.datetime(2015, 11, 3, 3, 2, 48))
def test_invalid_date(self):
self.assertRaises(errors.DateFormatError, cli.parse_date,
"invalid_format")
self.assertRaises(errors.DateFormatError, cli.parse_date, "invalid_format")
# test invalid buildid (43 is not a valid day)
self.assertRaises(errors.DateFormatError, cli.parse_date,
"20151143030248")
self.assertRaises(errors.DateFormatError, cli.parse_date, "20151143030248")
class TestParseBits(unittest.TestCase):
@patch('mozregression.cli.mozinfo')
@patch("mozregression.cli.mozinfo")
def test_parse_32(self, mozinfo):
mozinfo.bits = 32
self.assertEqual(cli.parse_bits('32'), 32)
self.assertEqual(cli.parse_bits('64'), 32)
self.assertEqual(cli.parse_bits("32"), 32)
self.assertEqual(cli.parse_bits("64"), 32)
@patch('mozregression.cli.mozinfo')
@patch("mozregression.cli.mozinfo")
def test_parse_64(self, mozinfo):
mozinfo.bits = 64
self.assertEqual(cli.parse_bits('32'), 32)
self.assertEqual(cli.parse_bits('64'), 64)
self.assertEqual(cli.parse_bits("32"), 32)
self.assertEqual(cli.parse_bits("64"), 64)
class TestPreferences(unittest.TestCase):
@ -54,18 +53,18 @@ class TestPreferences(unittest.TestCase):
handle, filepath = tempfile.mkstemp()
self.addCleanup(os.unlink, filepath)
with os.fdopen(handle, 'w') as conf_file:
with os.fdopen(handle, "w") as conf_file:
conf_file.write('{ "browser.tabs.remote.autostart": false }')
prefs_files = [filepath]
prefs = cli.preferences(prefs_files, None, None)
self.assertEqual(prefs, [('browser.tabs.remote.autostart', False)])
self.assertEqual(prefs, [("browser.tabs.remote.autostart", False)])
def test_preferences_args(self):
prefs_args = ["browser.tabs.remote.autostart:false"]
prefs = cli.preferences(None, prefs_args, None)
self.assertEqual(prefs, [('browser.tabs.remote.autostart', False)])
self.assertEqual(prefs, [("browser.tabs.remote.autostart", False)])
prefs_args = ["browser.tabs.remote.autostart"]
@ -78,41 +77,40 @@ class TestCli(unittest.TestCase):
handle, filepath = tempfile.mkstemp()
self.addCleanup(os.unlink, filepath)
with os.fdopen(handle, 'w') as conf_file:
with os.fdopen(handle, "w") as conf_file:
conf_file.write(content)
return filepath
def test_get_erronous_cfg_defaults(self):
filepath = self._create_conf_file('aaaaaaaaaaa [Defaults]\n')
filepath = self._create_conf_file("aaaaaaaaaaa [Defaults]\n")
with self.assertRaises(errors.MozRegressionError):
cli.cli(conf_file=filepath)
def test_get_defaults(self):
valid_values = {'http-timeout': '10.2',
'persist': '/home/foo/.mozregression',
'bits': '64'}
valid_values = {
"http-timeout": "10.2",
"persist": "/home/foo/.mozregression",
"bits": "64",
}
content = ["%s=%s\n" % (key, value)
for key, value in six.iteritems(valid_values)]
filepath = self._create_conf_file('\n'.join(content))
content = ["%s=%s\n" % (key, value) for key, value in six.iteritems(valid_values)]
filepath = self._create_conf_file("\n".join(content))
options = cli.cli(['--bits=32'], conf_file=filepath).options
options = cli.cli(["--bits=32"], conf_file=filepath).options
self.assertEqual(options.http_timeout, 10.2)
self.assertEqual(options.persist, '/home/foo/.mozregression')
self.assertEqual(options.bits, '32')
self.assertEqual(options.persist, "/home/foo/.mozregression")
self.assertEqual(options.bits, "32")
def test_warn_invalid_build_type_in_conf(self):
filepath = self._create_conf_file('build-type=foo\n')
filepath = self._create_conf_file("build-type=foo\n")
conf = cli.cli([], conf_file=filepath)
warns = []
conf.logger.warning = warns.append
conf.validate()
self.assertIn(
"Unable to find a suitable build type 'foo'."
" (Defaulting to 'shippable')",
warns
"Unable to find a suitable build type 'foo'." " (Defaulting to 'shippable')", warns,
)
@ -124,45 +122,45 @@ def do_cli(*argv):
def test_get_usage():
output = []
with patch('sys.stdout') as stdout:
with patch("sys.stdout") as stdout:
stdout.write.side_effect = output.append
with pytest.raises(SystemExit) as exc:
do_cli('-h')
do_cli("-h")
assert exc.value.code == 0
assert "usage:" in ''.join(output)
assert "usage:" in "".join(output)
def test_list_build_types(mocker):
output = []
with patch('sys.stdout') as stdout:
with patch("sys.stdout") as stdout:
stdout.write.side_effect = output.append
with pytest.raises(SystemExit) as exc:
do_cli('--list-build-types')
do_cli("--list-build-types")
assert exc.value.code == 0
assert "firefox:\n shippable" in ''.join(output)
assert "firefox:\n shippable" in "".join(output)
DEFAULTS_DATE = [
('linux', 64, datetime.date(2009, 1, 1)),
('linux', 32, datetime.date(2009, 1, 1)),
('mac', 64, datetime.date(2009, 1, 1)),
('win', 32, datetime.date(2009, 1, 1)),
('win', 64, datetime.date(2010, 5, 28)),
("linux", 64, datetime.date(2009, 1, 1)),
("linux", 32, datetime.date(2009, 1, 1)),
("mac", 64, datetime.date(2009, 1, 1)),
("win", 32, datetime.date(2009, 1, 1)),
("win", 64, datetime.date(2010, 5, 28)),
]
@pytest.mark.parametrize("os,bits,default_good_date", DEFAULTS_DATE)
def test_no_args(os, bits, default_good_date):
with patch('mozregression.cli.mozinfo') as mozinfo:
with patch("mozregression.cli.mozinfo") as mozinfo:
mozinfo.os = os
mozinfo.bits = bits
config = do_cli()
# application is by default firefox
assert config.fetch_config.app_name == 'firefox'
assert config.fetch_config.app_name == "firefox"
# nightly by default
assert config.action == 'bisect_nightlies'
assert config.action == "bisect_nightlies"
assert config.options.good == default_good_date
assert config.options.bad == datetime.date.today()
@ -172,72 +170,80 @@ SOME_DATE = TODAY + datetime.timedelta(days=-20)
SOME_OLDER_DATE = TODAY + datetime.timedelta(days=-10)
@pytest.mark.parametrize('params,good,bad', [
# we can use dates with integration branches
(['--good=%s' % SOME_DATE, '--bad=%s' % SOME_OLDER_DATE, '--repo=m-i'],
SOME_DATE, SOME_OLDER_DATE),
# non opt build flavors are also found using taskcluster
(['--good=%s' % SOME_DATE, '--bad=%s' % SOME_OLDER_DATE, '-B', 'debug'],
SOME_DATE, SOME_OLDER_DATE)
])
@pytest.mark.parametrize(
"params,good,bad",
[
# we can use dates with integration branches
(
["--good=%s" % SOME_DATE, "--bad=%s" % SOME_OLDER_DATE, "--repo=m-i"],
SOME_DATE,
SOME_OLDER_DATE,
),
# non opt build flavors are also found using taskcluster
(
["--good=%s" % SOME_DATE, "--bad=%s" % SOME_OLDER_DATE, "-B", "debug"],
SOME_DATE,
SOME_OLDER_DATE,
),
],
)
def test_use_taskcluster_bisection_method(params, good, bad):
config = do_cli(*params)
assert config.action == 'bisect_integration' # meaning taskcluster usage
assert config.action == "bisect_integration" # meaning taskcluster usage
# compare dates using the representation, as we may have
# date / datetime instances
assert config.options.good.strftime('%Y-%m-%d') \
== good.strftime('%Y-%m-%d')
assert config.options.bad.strftime('%Y-%m-%d') \
== bad.strftime('%Y-%m-%d')
assert config.options.good.strftime("%Y-%m-%d") == good.strftime("%Y-%m-%d")
assert config.options.bad.strftime("%Y-%m-%d") == bad.strftime("%Y-%m-%d")
@pytest.mark.parametrize("os,bits,default_bad_date", DEFAULTS_DATE)
def test_find_fix_reverse_default_dates(os, bits, default_bad_date):
with patch('mozregression.cli.mozinfo') as mozinfo:
with patch("mozregression.cli.mozinfo") as mozinfo:
mozinfo.os = os
mozinfo.bits = bits
config = do_cli('--find-fix')
config = do_cli("--find-fix")
# application is by default firefox
assert config.fetch_config.app_name == 'firefox'
assert config.fetch_config.app_name == "firefox"
# nightly by default
assert config.action == 'bisect_nightlies'
assert config.action == "bisect_nightlies"
assert config.options.bad == default_bad_date
assert config.options.good == datetime.date.today()
def test_with_releases():
releases_data = sorted(((k, v) for k, v in releases().items()),
key=(lambda k_v: k_v[0]))
conf = do_cli(
'--bad=%s' % releases_data[-1][0],
'--good=%s' % releases_data[0][0],
)
releases_data = sorted(((k, v) for k, v in releases().items()), key=(lambda k_v: k_v[0]))
conf = do_cli("--bad=%s" % releases_data[-1][0], "--good=%s" % releases_data[0][0],)
assert str(conf.options.good) == releases_data[0][1]
assert str(conf.options.bad) == releases_data[-1][1]
@pytest.mark.parametrize('args,action,value', [
(['--launch=34'], 'launch_nightlies', cli.parse_date(releases()[34])),
(['--launch=2015-11-01'], 'launch_nightlies', datetime.date(2015, 11, 1)),
(['--launch=abc123'], 'launch_integration', 'abc123'),
(['--launch=2015-11-01', '--repo=m-i'], 'launch_integration',
datetime.date(2015, 11, 1)),
])
@pytest.mark.parametrize(
"args,action,value",
[
(["--launch=34"], "launch_nightlies", cli.parse_date(releases()[34])),
(["--launch=2015-11-01"], "launch_nightlies", datetime.date(2015, 11, 1)),
(["--launch=abc123"], "launch_integration", "abc123"),
(["--launch=2015-11-01", "--repo=m-i"], "launch_integration", datetime.date(2015, 11, 1),),
],
)
def test_launch(args, action, value):
config = do_cli(*args)
assert config.action == action
assert config.options.launch == value
@pytest.mark.parametrize('args,repo,value', [
(['--launch=60.0', '--repo=m-r'], 'mozilla-release', 'FIREFOX_60_0_RELEASE'),
(['--launch=61', '--repo=m-r'], 'mozilla-release', 'FIREFOX_61_0_RELEASE'),
(['--launch=62.0.1'], 'mozilla-release', 'FIREFOX_62_0_1_RELEASE'),
(['--launch=63.0b4', '--repo=m-b'], 'mozilla-beta', 'FIREFOX_63_0b4_RELEASE'),
(['--launch=64', '--repo=m-b'], 'mozilla-beta', 'FIREFOX_RELEASE_64_BASE'),
(['--launch=65.0b11'], 'mozilla-beta', 'FIREFOX_65_0b11_RELEASE'),
])
@pytest.mark.parametrize(
"args,repo,value",
[
(["--launch=60.0", "--repo=m-r"], "mozilla-release", "FIREFOX_60_0_RELEASE"),
(["--launch=61", "--repo=m-r"], "mozilla-release", "FIREFOX_61_0_RELEASE"),
(["--launch=62.0.1"], "mozilla-release", "FIREFOX_62_0_1_RELEASE"),
(["--launch=63.0b4", "--repo=m-b"], "mozilla-beta", "FIREFOX_63_0b4_RELEASE"),
(["--launch=64", "--repo=m-b"], "mozilla-beta", "FIREFOX_RELEASE_64_BASE"),
(["--launch=65.0b11"], "mozilla-beta", "FIREFOX_65_0b11_RELEASE"),
],
)
def test_versions(args, repo, value):
config = do_cli(*args)
assert config.fetch_config.repo == repo
@ -246,84 +252,90 @@ def test_versions(args, repo, value):
def test_bad_date_later_than_good():
with pytest.raises(errors.MozRegressionError) as exc:
do_cli('--good=2015-01-01', '--bad=2015-01-10', '--find-fix')
assert 'is later than good' in str(exc.value)
do_cli("--good=2015-01-01", "--bad=2015-01-10", "--find-fix")
assert "is later than good" in str(exc.value)
assert "You should not use the --find-fix" in str(exc.value)
def test_good_date_later_than_bad():
with pytest.raises(errors.MozRegressionError) as exc:
do_cli('--good=2015-01-10', '--bad=2015-01-01')
assert 'is later than bad' in str(exc.value)
do_cli("--good=2015-01-10", "--bad=2015-01-01")
assert "is later than bad" in str(exc.value)
assert "you wanted to use the --find-fix" in str(exc.value)
def test_basic_integration():
config = do_cli('--good=c1', '--bad=c5')
assert config.fetch_config.app_name == 'firefox'
assert config.action == 'bisect_integration'
assert config.options.good == 'c1'
assert config.options.bad == 'c5'
config = do_cli("--good=c1", "--bad=c5")
assert config.fetch_config.app_name == "firefox"
assert config.action == "bisect_integration"
assert config.options.good == "c1"
assert config.options.bad == "c5"
def test_list_releases(mocker):
out = []
stdout = mocker.patch('sys.stdout')
stdout = mocker.patch("sys.stdout")
stdout.write = out.append
with pytest.raises(SystemExit):
do_cli('--list-releases')
assert 'Valid releases:' in '\n'.join(out)
do_cli("--list-releases")
assert "Valid releases:" in "\n".join(out)
def test_write_confif(mocker):
out = []
stdout = mocker.patch('sys.stdout')
stdout = mocker.patch("sys.stdout")
stdout.write = out.append
write_conf = mocker.patch('mozregression.cli.write_conf')
write_conf = mocker.patch("mozregression.cli.write_conf")
with pytest.raises(SystemExit):
do_cli('--write-conf')
do_cli("--write-conf")
assert len(write_conf.mock_calls) == 1
def test_warning_no_conf(mocker):
out = []
stdout = mocker.patch('sys.stdout')
stdout = mocker.patch("sys.stdout")
stdout.write = out.append
cli.cli([], conf_file='blah_this is not a valid file_I_hope')
assert "You should use a config file" in '\n'.join(out)
cli.cli([], conf_file="blah_this is not a valid file_I_hope")
assert "You should use a config file" in "\n".join(out)
@pytest.mark.parametrize('args,enabled', [
([], False), # not enabled by default, because build_type is opt
(['-P=stdout'], True), # explicitly enabled
(['-B=debug'], True), # enabled because build type is not opt
(['-B=debug', '-P=none'], False), # explicitly disabled
])
@pytest.mark.parametrize(
"args,enabled",
[
([], False), # not enabled by default, because build_type is opt
(["-P=stdout"], True), # explicitly enabled
(["-B=debug"], True), # enabled because build type is not opt
(["-B=debug", "-P=none"], False), # explicitly disabled
],
)
def test_process_output_enabled(args, enabled):
do_cli(*args)
log_filter = get_default_logger("process").component_filter
result = log_filter({'some': 'data'})
result = log_filter({"some": "data"})
if enabled:
assert result
else:
assert not result
@pytest.mark.parametrize('mozversion_msg,shown', [
("platform_changeset: abc123", False),
("application_changeset: abc123", True),
("application_version: stuff:thing", True),
("application_remotingname: stuff", False),
("application_id: stuff", False),
("application_vendor: stuff", False),
("application_display_name: stuff", False),
("not a valid key value pair", True),
])
@pytest.mark.parametrize(
"mozversion_msg,shown",
[
("platform_changeset: abc123", False),
("application_changeset: abc123", True),
("application_version: stuff:thing", True),
("application_remotingname: stuff", False),
("application_id: stuff", False),
("application_vendor: stuff", False),
("application_display_name: stuff", False),
("not a valid key value pair", True),
],
)
def test_mozversion_output_filtered(mozversion_msg, shown):
do_cli()
log_filter = get_default_logger("mozversion").component_filter
log_data = {'message': mozversion_msg}
log_data = {"message": mozversion_msg}
result = log_filter(log_data)
if shown:
@ -332,20 +344,22 @@ def test_mozversion_output_filtered(mozversion_msg, shown):
assert not result
@pytest.mark.parametrize('app, os, bits, processor, build_type, expected_range', [
('jsshell', 'win', 64, 'x86_64', None, (datetime.date(2014, 5, 27), TODAY)),
('jsshell', 'linux', 64, 'x86_64', 'asan', (datetime.date(2013, 9, 1), TODAY)),
('jsshell', 'linux', 64, 'x86_64', 'asan-debug', (datetime.date(2013, 9, 1), TODAY)),
('jsshell', 'linux', 32, 'x86', None, (datetime.date(2012, 4, 18), TODAY)),
('jsshell', 'mac', 64, 'x86_64', None, (datetime.date(2012, 4, 18), TODAY)),
('jsshell', 'win', 32, 'x86', None, (datetime.date(2012, 4, 18), TODAY)),
# anything else on win 64
('firefox', 'win', 64, 'x86_64', None, (datetime.date(2010, 5, 28), TODAY)),
# anything else
('firefox', 'linux', 64, 'x86_64', None, (datetime.date(2009, 1, 1), TODAY)),
])
def test_get_default_date_range(app, os, bits, processor, build_type,
expected_range):
@pytest.mark.parametrize(
"app, os, bits, processor, build_type, expected_range",
[
("jsshell", "win", 64, "x86_64", None, (datetime.date(2014, 5, 27), TODAY)),
("jsshell", "linux", 64, "x86_64", "asan", (datetime.date(2013, 9, 1), TODAY)),
("jsshell", "linux", 64, "x86_64", "asan-debug", (datetime.date(2013, 9, 1), TODAY),),
("jsshell", "linux", 32, "x86", None, (datetime.date(2012, 4, 18), TODAY)),
("jsshell", "mac", 64, "x86_64", None, (datetime.date(2012, 4, 18), TODAY)),
("jsshell", "win", 32, "x86", None, (datetime.date(2012, 4, 18), TODAY)),
# anything else on win 64
("firefox", "win", 64, "x86_64", None, (datetime.date(2010, 5, 28), TODAY)),
# anything else
("firefox", "linux", 64, "x86_64", None, (datetime.date(2009, 1, 1), TODAY)),
],
)
def test_get_default_date_range(app, os, bits, processor, build_type, expected_range):
fetch_config = create_config(app, os, bits, processor)
if build_type:
fetch_config.set_build_type(build_type)

Просмотреть файл

@ -1,10 +1,12 @@
from __future__ import absolute_import
import pytest
import os
import mozfile
import tempfile
from mozregression.config import write_conf, get_defaults
import mozfile
import pytest
from mozregression.config import get_defaults, write_conf
@pytest.yield_fixture
@ -14,33 +16,43 @@ def tmp():
mozfile.remove(temp_dir)
@pytest.mark.parametrize('os_,bits,inputs,conf_dir_exists,results', [
('mac', 64, ['', ''], False,
{'persist': None, 'persist-size-limit': '20.0'}),
('linux', 32, ['NONE', 'NONE'], True,
{'persist': '', 'persist-size-limit': '0.0'}),
('win', 32, ['', '10'], True,
{'persist': None, 'persist-size-limit': '10.0'}),
# on 64 bit (except for mac), bits option is asked
('linu', 64, ['NONE', 'NONE', ''], True,
{'persist': '', 'persist-size-limit': '0.0', 'bits': '64'}),
('win', 64, ['NONE', 'NONE', '32'], True,
{'persist': '', 'persist-size-limit': '0.0', 'bits': '32'}),
])
@pytest.mark.parametrize(
"os_,bits,inputs,conf_dir_exists,results",
[
("mac", 64, ["", ""], False, {"persist": None, "persist-size-limit": "20.0"}),
("linux", 32, ["NONE", "NONE"], True, {"persist": "", "persist-size-limit": "0.0"},),
("win", 32, ["", "10"], True, {"persist": None, "persist-size-limit": "10.0"}),
# on 64 bit (except for mac), bits option is asked
(
"linu",
64,
["NONE", "NONE", ""],
True,
{"persist": "", "persist-size-limit": "0.0", "bits": "64"},
),
(
"win",
64,
["NONE", "NONE", "32"],
True,
{"persist": "", "persist-size-limit": "0.0", "bits": "32"},
),
],
)
def test_write_conf(tmp, mocker, os_, bits, inputs, conf_dir_exists, results):
mozinfo = mocker.patch('mozregression.config.mozinfo')
mozinfo = mocker.patch("mozregression.config.mozinfo")
mozinfo.os = os_
mozinfo.bits = bits
mocked_input = mocker.patch('mozregression.config.input')
mocked_input = mocker.patch("mozregression.config.input")
mocked_input.return_value = ""
mocked_input.side_effect = inputs
conf_path = os.path.join(tmp, 'conf.cfg')
conf_path = os.path.join(tmp, "conf.cfg")
if not conf_dir_exists:
mozfile.remove(conf_path)
write_conf(conf_path)
if 'persist' in results and results['persist'] is None:
if "persist" in results and results["persist"] is None:
# default persist is base on the directory of the conf file
results['persist'] = os.path.join(tmp, 'persist')
results["persist"] = os.path.join(tmp, "persist")
conf = get_defaults(conf_path)
for key in results:
assert conf[key] == results[key]
@ -50,9 +62,9 @@ def test_write_conf(tmp, mocker, os_, bits, inputs, conf_dir_exists, results):
def test_write_existing_conf(tmp, mocker):
mocked_input = mocker.patch('mozregression.config.input')
mocked_input = mocker.patch("mozregression.config.input")
mocked_input.return_value = ""
conf_path = os.path.join(tmp, 'conf.cfg')
conf_path = os.path.join(tmp, "conf.cfg")
write_conf(conf_path)
results = get_defaults(conf_path)
assert results

Просмотреть файл

@ -3,12 +3,14 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
from __future__ import absolute_import
import unittest
import tempfile
import shutil
import os
import shutil
import tempfile
import time
from mock import Mock, patch, ANY
import unittest
from mock import ANY, Mock, patch
from mozregression import download_manager
@ -28,7 +30,7 @@ def mock_response(response, data, wait=0):
rest = rest[chunk_size:]
yield chunk
response.headers = {'Content-length': str(len(data))}
response.headers = {"Content-length": str(len(data))}
response.iter_content = iter_content
@ -38,24 +40,27 @@ class TestDownload(unittest.TestCase):
self.addCleanup(shutil.rmtree, self.tempdir)
self.finished = Mock()
self.session, self.session_response = mock_session()
self.tempfile = os.path.join(self.tempdir, 'dest')
self.dl = download_manager.Download('http://url', self.tempfile,
finished_callback=self.finished,
chunk_size=4,
session=self.session)
self.tempfile = os.path.join(self.tempdir, "dest")
self.dl = download_manager.Download(
"http://url",
self.tempfile,
finished_callback=self.finished,
chunk_size=4,
session=self.session,
)
def test_creation(self):
self.assertFalse(self.dl.is_canceled())
self.assertFalse(self.dl.is_running())
self.assertIsNone(self.dl.error())
self.assertEqual(self.dl.get_url(), 'http://url')
self.assertEqual(self.dl.get_url(), "http://url")
self.assertEqual(self.dl.get_dest(), self.tempfile)
def create_response(self, data, wait=0):
mock_response(self.session_response, data, wait)
def test_download(self):
self.create_response(b'1234' * 4, 0.01)
self.create_response(b"1234" * 4, 0.01)
# no file present yet
self.assertFalse(os.path.exists(self.tempfile))
@ -68,10 +73,10 @@ class TestDownload(unittest.TestCase):
self.finished.assert_called_with(self.dl)
# file has been downloaded
with open(self.tempfile) as f:
self.assertEqual(f.read(), '1234' * 4)
self.assertEqual(f.read(), "1234" * 4)
def test_download_cancel(self):
self.create_response(b'1234' * 1000, wait=0.01)
self.create_response(b"1234" * 1000, wait=0.01)
start = time.time()
self.dl.start()
@ -98,27 +103,30 @@ class TestDownload(unittest.TestCase):
def update_progress(_dl, current, total):
data.append((_dl, current, total))
self.create_response(b'1234' * 4)
self.create_response(b"1234" * 4)
self.dl.set_progress(update_progress)
self.dl.start()
self.dl.wait()
self.assertEqual(data, [
(self.dl, 0, 16),
(self.dl, 4, 16),
(self.dl, 8, 16),
(self.dl, 12, 16),
(self.dl, 16, 16),
])
self.assertEqual(
data,
[
(self.dl, 0, 16),
(self.dl, 4, 16),
(self.dl, 8, 16),
(self.dl, 12, 16),
(self.dl, 16, 16),
],
)
# file has been downloaded
with open(self.tempfile) as f:
self.assertEqual(f.read(), '1234' * 4)
self.assertEqual(f.read(), "1234" * 4)
# finished callback was called
self.finished.assert_called_with(self.dl)
def test_download_error_in_thread(self):
self.session_response.headers = {'Content-length': '24'}
self.session_response.headers = {"Content-length": "24"}
self.session_response.iter_content.side_effect = IOError
self.dl.start()
@ -132,10 +140,10 @@ class TestDownload(unittest.TestCase):
def test_wait_does_not_block_on_exception(self):
# this test the case when a user may hit CTRL-C for example
# during a dl.wait() call.
self.create_response(b'1234' * 1000, wait=0.01)
self.create_response(b"1234" * 1000, wait=0.01)
original_join = self.dl.thread.join
it = iter('123')
it = iter("123")
def join(timeout=None):
next(it) # will throw StopIteration after a few calls
@ -178,17 +186,17 @@ class TestDownloadManager(unittest.TestCase):
return self.dl_manager.download(url, fname)
def test_download(self):
dl1 = self.do_download('http://foo', 'foo', b'hello' * 4, wait=0.02)
dl1 = self.do_download("http://foo", "foo", b"hello" * 4, wait=0.02)
self.assertIsInstance(dl1, download_manager.Download)
self.assertTrue(dl1.is_running())
# with the same fname, no new download is started. The same instance
# is returned since the download is running.
dl2 = self.do_download('http://bar', 'foo', b'hello2' * 4, wait=0.02)
dl2 = self.do_download("http://bar", "foo", b"hello2" * 4, wait=0.02)
self.assertEqual(dl1, dl2)
# starting a download with another fname will trigger a new download
dl3 = self.do_download('http://bar', 'foo2', b'hello you' * 4)
dl3 = self.do_download("http://bar", "foo2", b"hello you" * 4)
self.assertIsInstance(dl3, download_manager.Download)
self.assertNotEqual(dl3, dl1)
@ -197,28 +205,30 @@ class TestDownloadManager(unittest.TestCase):
dl1.wait()
# now if we try to download a fname that exists, None is returned
dl4 = self.do_download('http://bar', 'foo', b'hello2' * 4, wait=0.02)
dl4 = self.do_download("http://bar", "foo", b"hello2" * 4, wait=0.02)
self.assertIsNone(dl4)
# downloaded files are what is expected
def content(fname):
with open(os.path.join(self.tempdir, fname)) as f:
return f.read()
self.assertEqual(content('foo'), 'hello' * 4)
self.assertEqual(content('foo2'), 'hello you' * 4)
self.assertEqual(content("foo"), "hello" * 4)
self.assertEqual(content("foo2"), "hello you" * 4)
# download instances are removed from the manager (internal test)
self.assertEqual(self.dl_manager._downloads, {})
def test_cancel(self):
dl1 = self.do_download('http://foo', 'foo', b'foo' * 50000, wait=0.02)
dl2 = self.do_download('http://foo', 'bar', b'bar' * 50000, wait=0.02)
dl3 = self.do_download('http://foo', 'foobar', b'foobar' * 4)
dl1 = self.do_download("http://foo", "foo", b"foo" * 50000, wait=0.02)
dl2 = self.do_download("http://foo", "bar", b"bar" * 50000, wait=0.02)
dl3 = self.do_download("http://foo", "foobar", b"foobar" * 4)
# let's cancel only one
def cancel_if(dl):
if os.path.basename(dl.get_dest()) == 'foo':
if os.path.basename(dl.get_dest()) == "foo":
return True
self.dl_manager.cancel(cancel_if=cancel_if)
self.assertTrue(dl1.is_canceled())
@ -243,8 +253,8 @@ class TestDownloadManager(unittest.TestCase):
# at the end, only dl3 has been downloaded
self.assertEqual(os.listdir(self.tempdir), ["foobar"])
with open(os.path.join(self.tempdir, 'foobar')) as f:
self.assertEqual(f.read(), 'foobar' * 4)
with open(os.path.join(self.tempdir, "foobar")) as f:
self.assertEqual(f.read(), "foobar" * 4)
# download instances are removed from the manager (internal test)
self.assertEqual(self.dl_manager._downloads, {})
@ -261,46 +271,44 @@ class TestDownloadProgress(unittest.TestCase):
class TestBuildDownloadManager(unittest.TestCase):
def setUp(self):
self.session, self.session_response = mock_session()
self.dl_manager = \
download_manager.BuildDownloadManager('dest',
session=self.session)
self.dl_manager = download_manager.BuildDownloadManager("dest", session=self.session)
self.dl_manager.logger = Mock()
def test__extract_download_info(self):
url, fname = self.dl_manager._extract_download_info(Mock(**{
'build_url': 'http://some/thing',
'persist_filename': '2015-01-03--my-repo--thing'
}))
self.assertEqual(url, 'http://some/thing')
self.assertEqual(fname, '2015-01-03--my-repo--thing')
url, fname = self.dl_manager._extract_download_info(
Mock(
**{
"build_url": "http://some/thing",
"persist_filename": "2015-01-03--my-repo--thing",
}
)
)
self.assertEqual(url, "http://some/thing")
self.assertEqual(fname, "2015-01-03--my-repo--thing")
@patch("mozregression.download_manager.BuildDownloadManager."
"_extract_download_info")
@patch("mozregression.download_manager.BuildDownloadManager." "_extract_download_info")
@patch("mozregression.download_manager.BuildDownloadManager.download")
def test_download_in_background(self, download, extract):
extract.return_value = ('http://foo/bar', 'myfile')
extract.return_value = ("http://foo/bar", "myfile")
download.return_value = ANY
result = self.dl_manager.download_in_background({'build': 'info'})
result = self.dl_manager.download_in_background({"build": "info"})
extract.assert_called_with({'build': 'info'})
download.assert_called_with('http://foo/bar', 'myfile')
self.assertIn('myfile', self.dl_manager._downloads_bg)
extract.assert_called_with({"build": "info"})
download.assert_called_with("http://foo/bar", "myfile")
self.assertIn("myfile", self.dl_manager._downloads_bg)
self.assertEqual(result, ANY)
@patch('mozregression.download_manager.LOG')
@patch("mozregression.download_manager.BuildDownloadManager."
"_extract_download_info")
@patch("mozregression.download_manager.LOG")
@patch("mozregression.download_manager.BuildDownloadManager." "_extract_download_info")
def _test_focus_download(self, other_canceled, extract, log):
extract.return_value = ('http://foo/bar', 'myfile')
current_dest = os.path.join('dest', 'myfile')
other_dest = os.path.join('dest', 'otherfile')
curent_download = download_manager.Download('http://url',
current_dest)
extract.return_value = ("http://foo/bar", "myfile")
current_dest = os.path.join("dest", "myfile")
other_dest = os.path.join("dest", "otherfile")
curent_download = download_manager.Download("http://url", current_dest)
curent_download.wait = Mock()
curent_download.set_progress = Mock()
other_download = download_manager.Download('http://url',
other_dest)
other_download = download_manager.Download("http://url", other_dest)
# fake some download activity
self.dl_manager._downloads = {
current_dest: curent_download,
@ -309,7 +317,7 @@ class TestBuildDownloadManager(unittest.TestCase):
curent_download.is_running = Mock(return_value=True)
other_download.is_running = Mock(return_value=True)
build_info = Mock(build='info')
build_info = Mock(build="info")
result = self.dl_manager.focus_download(build_info)
self.assertFalse(curent_download.is_canceled())
@ -317,8 +325,7 @@ class TestBuildDownloadManager(unittest.TestCase):
self.assertEqual(other_download.is_canceled(), other_canceled)
log.info.assert_called_with(
"Downloading build from: http://foo/bar")
log.info.assert_called_with("Downloading build from: http://foo/bar")
self.assertEqual(result, current_dest)
self.assertEqual(result, build_info.build_file)
@ -330,23 +337,21 @@ class TestBuildDownloadManager(unittest.TestCase):
self.dl_manager.background_dl_policy = "keep"
self._test_focus_download(False)
@patch('mozregression.download_manager.LOG')
@patch("mozregression.download_manager.BuildDownloadManager."
"_extract_download_info")
@patch("mozregression.download_manager.LOG")
@patch("mozregression.download_manager.BuildDownloadManager." "_extract_download_info")
@patch("mozregression.download_manager.BuildDownloadManager.download")
def test_focus_download_file_already_exists(self, download, extract, log):
extract.return_value = ('http://foo/bar', 'myfile')
extract.return_value = ("http://foo/bar", "myfile")
download.return_value = None
# fake that we downloaded that in background
self.dl_manager._downloads_bg.add('myfile')
self.dl_manager._downloads_bg.add("myfile")
build_info = Mock(build='info')
build_info = Mock(build="info")
result = self.dl_manager.focus_download(build_info)
dest_file = os.path.join('dest', 'myfile')
log.info.assert_called_with(
"Using local file: %s (downloaded in background)" % dest_file)
dest_file = os.path.join("dest", "myfile")
log.info.assert_called_with("Using local file: %s (downloaded in background)" % dest_file)
self.assertEqual(result, dest_file)
self.assertEqual(result, build_info.build_file)

Просмотреть файл

@ -1,67 +1,67 @@
from __future__ import absolute_import
import re
import unittest
import datetime
from mock import patch, Mock
import re
import time
import unittest
from mock import Mock, patch
from mozregression import config, errors, fetch_build_info, fetch_configs
from mozregression import config, fetch_build_info, fetch_configs, errors
from .test_fetch_configs import create_push
class TestInfoFetcher(unittest.TestCase):
def setUp(self):
fetch_config = fetch_configs.create_config('firefox', 'linux', 64,
'x86_64')
fetch_config = fetch_configs.create_config("firefox", "linux", 64, "x86_64")
self.info_fetcher = fetch_build_info.InfoFetcher(fetch_config)
@patch('requests.get')
@patch("requests.get")
def test__fetch_txt_info(self, get):
response = Mock(text="20141101030205\nhttps://hg.mozilla.org/\
mozilla-central/rev/b695d9575654\n")
response = Mock(
text="20141101030205\nhttps://hg.mozilla.org/\
mozilla-central/rev/b695d9575654\n"
)
get.return_value = response
expected = {
'repository': 'https://hg.mozilla.org/mozilla-central',
'changeset': 'b695d9575654',
"repository": "https://hg.mozilla.org/mozilla-central",
"changeset": "b695d9575654",
}
self.assertEqual(self.info_fetcher._fetch_txt_info('http://foo.txt'),
expected)
self.assertEqual(self.info_fetcher._fetch_txt_info("http://foo.txt"), expected)
@patch('requests.get')
@patch("requests.get")
def test__fetch_txt_info_old_format(self, get):
response = Mock(text="20110126030333 e0fc18b3bc41\n")
get.return_value = response
expected = {
'changeset': 'e0fc18b3bc41',
"changeset": "e0fc18b3bc41",
}
self.assertEqual(self.info_fetcher._fetch_txt_info('http://foo.txt'),
expected)
self.assertEqual(self.info_fetcher._fetch_txt_info("http://foo.txt"), expected)
class TestNightlyInfoFetcher(unittest.TestCase):
def setUp(self):
fetch_config = fetch_configs.create_config('firefox', 'linux', 64,
'x86_64')
fetch_config = fetch_configs.create_config("firefox", "linux", 64, "x86_64")
self.info_fetcher = fetch_build_info.NightlyInfoFetcher(fetch_config)
@patch('mozregression.fetch_build_info.url_links')
@patch("mozregression.fetch_build_info.url_links")
def test__find_build_info_from_url(self, url_links):
url_links.return_value = [
'file1.txt.gz',
'file2.txt',
'firefox01linux-x86_64.txt',
'firefox01linux-x86_64.tar.bz2',
"file1.txt.gz",
"file2.txt",
"firefox01linux-x86_64.txt",
"firefox01linux-x86_64.tar.bz2",
]
expected = {
'build_txt_url': 'http://foo/firefox01linux-x86_64.txt',
'build_url': 'http://foo/firefox01linux-x86_64.tar.bz2',
"build_txt_url": "http://foo/firefox01linux-x86_64.txt",
"build_url": "http://foo/firefox01linux-x86_64.tar.bz2",
}
builds = []
self.info_fetcher._fetch_build_info_from_url('http://foo', 0, builds)
self.info_fetcher._fetch_build_info_from_url("http://foo", 0, builds)
self.assertEqual(builds, [(0, expected)])
@patch('mozregression.fetch_build_info.url_links')
@patch("mozregression.fetch_build_info.url_links")
def test__find_build_info_incomplete_data_raises_exception(self, url_links):
# We want to find a valid match for one of the build file regexes,
# build_info_regex. But we will make the build filename regex fail. This
@ -70,7 +70,7 @@ class TestNightlyInfoFetcher(unittest.TestCase):
# regex.
url_links.return_value = [
"validinfofilename.txt",
"invalidbuildfilename.tar.bz2"
"invalidbuildfilename.tar.bz2",
]
# build_regex doesn't match any of the files in the web directory.
self.info_fetcher.build_regex = re.compile("xxx")
@ -80,47 +80,46 @@ class TestNightlyInfoFetcher(unittest.TestCase):
with self.assertRaises(errors.BuildInfoNotFound):
self.info_fetcher._fetch_build_info_from_url("some-url", 1, [])
@patch('mozregression.fetch_build_info.url_links')
@patch("mozregression.fetch_build_info.url_links")
def test__get_url(self, url_links):
url_links.return_value = [
'2014-11-01-03-02-05-mozilla-central/',
'2014-11-01-03-02-05-foo/',
'foo',
'bar/'
"2014-11-01-03-02-05-mozilla-central/",
"2014-11-01-03-02-05-foo/",
"foo",
"bar/",
]
urls = self.info_fetcher._get_urls(datetime.date(2014, 11, 0o1))
self.assertEqual(
urls[0],
fetch_configs.ARCHIVE_BASE_URL +
'/firefox/nightly/2014/11/2014-11-01-03-02-05-mozilla-central/')
fetch_configs.ARCHIVE_BASE_URL
+ "/firefox/nightly/2014/11/2014-11-01-03-02-05-mozilla-central/",
)
urls = self.info_fetcher._get_urls(datetime.date(2014, 11, 0o2))
self.assertEqual(urls, [])
def test_find_build_info(self):
get_urls = self.info_fetcher._get_urls = Mock(return_value=[
'https://archive.mozilla.org/pub/mozilla.org/\
bar/nightly/2014/11/2014-11-15-08-02-05-mozilla-central/',
'https://archive.mozilla.org/pub/mozilla.org/\
bar/nightly/2014/11/2014-11-15-04-02-05-mozilla-central/',
'https://archive.mozilla.org/pub/mozilla.org/\
bar/nightly/2014/11/2014-11-15-03-02-05-mozilla-central',
'https://archive.mozilla.org/pub/mozilla.org/\
bar/nightly/2014/11/2014-11-15-02-02-05-mozilla-central/',
'https://archive.mozilla.org/pub/mozilla.org/\
bar/nightly/2014/11/2014-11-15-01-02-05-mozilla-central/',
])
get_urls = self.info_fetcher._get_urls = Mock(
return_value=[
"https://archive.mozilla.org/pub/mozilla.org/\
bar/nightly/2014/11/2014-11-15-08-02-05-mozilla-central/",
"https://archive.mozilla.org/pub/mozilla.org/\
bar/nightly/2014/11/2014-11-15-04-02-05-mozilla-central/",
"https://archive.mozilla.org/pub/mozilla.org/\
bar/nightly/2014/11/2014-11-15-03-02-05-mozilla-central",
"https://archive.mozilla.org/pub/mozilla.org/\
bar/nightly/2014/11/2014-11-15-02-02-05-mozilla-central/",
"https://archive.mozilla.org/pub/mozilla.org/\
bar/nightly/2014/11/2014-11-15-01-02-05-mozilla-central/",
]
)
def my_find_build_info(url, index, lst):
# say only the last build url is invalid
if url in get_urls.return_value[:-1]:
return
lst.append((index, {
'build_txt_url': url,
'build_url': url,
}))
self.info_fetcher._fetch_build_info_from_url = Mock(
side_effect=my_find_build_info
)
lst.append((index, {"build_txt_url": url, "build_url": url}))
self.info_fetcher._fetch_build_info_from_url = Mock(side_effect=my_find_build_info)
self.info_fetcher._fetch_txt_info = Mock(return_value={})
result = self.info_fetcher.find_build_info(datetime.date(2014, 11, 15))
# we must have found the last build url valid
@ -134,72 +133,64 @@ bar/nightly/2014/11/2014-11-15-01-02-05-mozilla-central/',
class TestIntegrationInfoFetcher(unittest.TestCase):
def setUp(self):
fetch_config = fetch_configs.create_config('firefox', 'linux', 64,
'x86_64')
fetch_config = fetch_configs.create_config("firefox", "linux", 64, "x86_64")
self.info_fetcher = fetch_build_info.IntegrationInfoFetcher(fetch_config)
@patch('taskcluster.Index')
@patch('taskcluster.Queue')
@patch("taskcluster.Index")
@patch("taskcluster.Queue")
def test_find_build_info(self, Queue, Index):
Index.return_value.findTask.return_value = {'taskId': 'task1'}
Index.return_value.findTask.return_value = {"taskId": "task1"}
Queue.return_value.status.return_value = {
"status": {"runs": [{
"state": "completed",
"runId": 0,
"resolved": '2015-06-01T22:13:02.115Z'
}]}
"status": {
"runs": [{"state": "completed", "runId": 0, "resolved": "2015-06-01T22:13:02.115Z"}]
}
}
Queue.return_value.listArtifacts.return_value = {
"artifacts": [
# return two valid artifact names
{'name': 'firefox-42.0a1.en-US.linux-x86_64.tar.bz2'},
{'name': 'firefox-42.0a1.en-US.linux-x86_64.txt'},
{"name": "firefox-42.0a1.en-US.linux-x86_64.tar.bz2"},
{"name": "firefox-42.0a1.en-US.linux-x86_64.txt"},
]
}
Queue.return_value.buildUrl.return_value = (
'http://firefox-42.0a1.en-US.linux-x86_64.tar.bz2'
"http://firefox-42.0a1.en-US.linux-x86_64.tar.bz2"
)
self.info_fetcher._fetch_txt_info = \
Mock(return_value={'changeset': '123456789'})
self.info_fetcher._fetch_txt_info = Mock(return_value={"changeset": "123456789"})
# test that we start searching using the correct tc root url
for push_timestamp in [
0,
time.mktime(
config.TC_ROOT_URL_MIGRATION_FLAG_DATE.timetuple()) + 100
0,
time.mktime(config.TC_ROOT_URL_MIGRATION_FLAG_DATE.timetuple()) + 100,
]:
result = self.info_fetcher.find_build_info(
create_push('123456789', push_timestamp))
result = self.info_fetcher.find_build_info(create_push("123456789", push_timestamp))
if push_timestamp == 0:
Index.assert_called_with({'rootUrl': config.OLD_TC_ROOT_URL})
Index.assert_called_with({"rootUrl": config.OLD_TC_ROOT_URL})
else:
Index.assert_called_with({'rootUrl': config.TC_ROOT_URL})
self.assertEqual(result.build_url,
'http://firefox-42.0a1.en-US.linux-x86_64.tar.bz2')
self.assertEqual(result.changeset, '123456789')
Index.assert_called_with({"rootUrl": config.TC_ROOT_URL})
self.assertEqual(result.build_url, "http://firefox-42.0a1.en-US.linux-x86_64.tar.bz2")
self.assertEqual(result.changeset, "123456789")
self.assertEqual(result.build_type, "integration")
@patch('taskcluster.Index')
@patch("taskcluster.Index")
def test_find_build_info_no_task(self, Index):
Index.findTask = Mock(
side_effect=fetch_build_info.TaskclusterFailure
)
Index.findTask = Mock(side_effect=fetch_build_info.TaskclusterFailure)
with self.assertRaises(errors.BuildInfoNotFound):
self.info_fetcher.find_build_info(
create_push('123456789', 1))
self.info_fetcher.find_build_info(create_push("123456789", 1))
@patch('taskcluster.Index')
@patch('taskcluster.Queue')
@patch("taskcluster.Index")
@patch("taskcluster.Queue")
def test_get_valid_build_no_artifacts(self, Queue, Index):
def find_task(route):
return {'taskId': 'task1'}
return {"taskId": "task1"}
def status(task_id):
return {"status": {"runs": [{
"state": "completed",
"runId": 0,
"resolved": '2015-06-01T22:13:02.115Z'
}]}}
return {
"status": {
"runs": [
{"state": "completed", "runId": 0, "resolved": "2015-06-01T22:13:02.115Z"}
]
}
}
def list_artifacts(taskid, run_id):
return {"artifacts": []}
@ -209,12 +200,11 @@ class TestIntegrationInfoFetcher(unittest.TestCase):
Queue.listArtifacts = list_artifacts
with self.assertRaises(errors.BuildInfoNotFound):
self.info_fetcher.find_build_info(
create_push('123456789', 1))
self.info_fetcher.find_build_info(create_push("123456789", 1))
@patch('mozregression.json_pushes.JsonPushes.push')
@patch("mozregression.json_pushes.JsonPushes.push")
def test_find_build_info_check_changeset_error(self, push):
push.side_effect = errors.MozRegressionError
with self.assertRaises(errors.BuildInfoNotFound):
self.info_fetcher.find_build_info('123456789',)
push.assert_called_with('123456789')
self.info_fetcher.find_build_info("123456789",)
push.assert_called_with("123456789")

Просмотреть файл

@ -1,43 +1,43 @@
from __future__ import absolute_import
import unittest
import datetime
import re
import unittest
import pytest
from mozregression.dates import to_utc_timestamp
from mozregression.json_pushes import Push
from mozregression.fetch_configs import (FirefoxConfig, create_config, errors,
get_build_regex, ARCHIVE_BASE_URL,
TIMESTAMP_FENNEC_API_15,
TIMESTAMP_FENNEC_API_16)
TIMESTAMP_TEST = to_utc_timestamp(
datetime.datetime(2017, 11, 14, 0, 0, 0)
from mozregression.fetch_configs import (
ARCHIVE_BASE_URL,
TIMESTAMP_FENNEC_API_15,
TIMESTAMP_FENNEC_API_16,
FirefoxConfig,
create_config,
errors,
get_build_regex,
)
from mozregression.json_pushes import Push
TIMESTAMP_TEST = to_utc_timestamp(datetime.datetime(2017, 11, 14, 0, 0, 0))
def create_push(chset, timestamp):
return Push(1, {
'changesets': [chset],
'date': timestamp
})
return Push(1, {"changesets": [chset], "date": timestamp})
class TestFirefoxConfigLinux64(unittest.TestCase):
app_name = 'firefox'
os = 'linux'
app_name = "firefox"
os = "linux"
bits = 64
processor = 'x86_64'
processor = "x86_64"
build_examples = ['firefox-38.0a1.en-US.linux-x86_64.tar.bz2']
build_info_examples = ['firefox-38.0a1.en-US.linux-x86_64.txt']
build_examples = ["firefox-38.0a1.en-US.linux-x86_64.tar.bz2"]
build_info_examples = ["firefox-38.0a1.en-US.linux-x86_64.txt"]
instance_type = FirefoxConfig
def setUp(self):
self.conf = create_config(self.app_name, self.os, self.bits,
self.processor)
self.conf = create_config(self.app_name, self.os, self.bits, self.processor)
def test_instance(self):
self.assertIsInstance(self.conf, self.instance_type)
@ -53,153 +53,137 @@ class TestFirefoxConfigLinux64(unittest.TestCase):
self.assertIsNotNone(res)
def test_get_nighly_base_url(self):
base_url = self.conf.get_nighly_base_url(datetime.date(2008,
6, 27))
self.assertEqual(base_url,
ARCHIVE_BASE_URL + '/firefox/nightly/2008/06/')
base_url = self.conf.get_nighly_base_url(datetime.date(2008, 6, 27))
self.assertEqual(base_url, ARCHIVE_BASE_URL + "/firefox/nightly/2008/06/")
def test_get_nightly_base_url_with_specific_base(self):
self.conf.set_base_url("http://ftp-origin-scl3.mozilla.org/pub/")
self.assertEqual(
"http://ftp-origin-scl3.mozilla.org/pub/firefox/nightly/2008/06/",
self.conf.get_nighly_base_url(datetime.date(2008, 6, 27))
self.conf.get_nighly_base_url(datetime.date(2008, 6, 27)),
)
def test_nightly_repo_regex(self):
repo_regex = self.conf.get_nightly_repo_regex(datetime.date(2008,
6, 15))
self.assertEqual(repo_regex, '^2008-06-15-[\\d-]+trunk/$')
repo_regex = self.conf.get_nightly_repo_regex(datetime.date(2008,
6, 27))
self.assertEqual(repo_regex, '^2008-06-27-[\\d-]+mozilla-central/$')
repo_regex = self.conf.get_nightly_repo_regex(datetime.date(2008, 6, 15))
self.assertEqual(repo_regex, "^2008-06-15-[\\d-]+trunk/$")
repo_regex = self.conf.get_nightly_repo_regex(datetime.date(2008, 6, 27))
self.assertEqual(repo_regex, "^2008-06-27-[\\d-]+mozilla-central/$")
# test with a datetime instance (buildid)
repo_regex = self.conf.get_nightly_repo_regex(
datetime.datetime(2015, 11, 27, 6, 5, 58))
self.assertEqual(repo_regex, '^2015-11-27-06-05-58-mozilla-central/$')
repo_regex = self.conf.get_nightly_repo_regex(datetime.datetime(2015, 11, 27, 6, 5, 58))
self.assertEqual(repo_regex, "^2015-11-27-06-05-58-mozilla-central/$")
def test_set_repo(self):
self.conf.set_repo('foo-bar')
repo_regex = self.conf.get_nightly_repo_regex(datetime.date(2008,
6, 27))
self.assertEqual(repo_regex, '^2008-06-27-[\\d-]+foo-bar/$')
self.conf.set_repo("foo-bar")
repo_regex = self.conf.get_nightly_repo_regex(datetime.date(2008, 6, 27))
self.assertEqual(repo_regex, "^2008-06-27-[\\d-]+foo-bar/$")
# with a value of None, default is applied
self.conf.set_repo(None)
repo_regex = self.conf.get_nightly_repo_regex(datetime.date(2008,
6, 27))
self.assertEqual(repo_regex, '^2008-06-27-[\\d-]+mozilla-central/$')
repo_regex = self.conf.get_nightly_repo_regex(datetime.date(2008, 6, 27))
self.assertEqual(repo_regex, "^2008-06-27-[\\d-]+mozilla-central/$")
class TestFirefoxConfigLinux32(TestFirefoxConfigLinux64):
bits = 32
processor = 'x86'
build_examples = ['firefox-38.0a1.en-US.linux-i686.tar.bz2']
build_info_examples = ['firefox-38.0a1.en-US.linux-i686.txt']
processor = "x86"
build_examples = ["firefox-38.0a1.en-US.linux-i686.tar.bz2"]
build_info_examples = ["firefox-38.0a1.en-US.linux-i686.txt"]
class TestFirefoxConfigWin64(TestFirefoxConfigLinux64):
os = 'win'
build_examples = ['firefox-38.0a1.en-US.win64-x86_64.zip',
'firefox-38.0a1.en-US.win64.zip']
build_info_examples = ['firefox-38.0a1.en-US.win64-x86_64.txt',
'firefox-38.0a1.en-US.win64.txt']
os = "win"
build_examples = [
"firefox-38.0a1.en-US.win64-x86_64.zip",
"firefox-38.0a1.en-US.win64.zip",
]
build_info_examples = [
"firefox-38.0a1.en-US.win64-x86_64.txt",
"firefox-38.0a1.en-US.win64.txt",
]
class TestFirefoxConfigWin32(TestFirefoxConfigWin64):
bits = 32
processor = 'x86'
build_examples = ['firefox-38.0a1.en-US.win32.zip']
build_info_examples = ['firefox-38.0a1.en-US.win32.txt']
processor = "x86"
build_examples = ["firefox-38.0a1.en-US.win32.zip"]
build_info_examples = ["firefox-38.0a1.en-US.win32.txt"]
class TestFirefoxConfigMac(TestFirefoxConfigLinux64):
os = 'mac'
build_examples = ['firefox-38.0a1.en-US.mac.dmg']
build_info_examples = ['firefox-38.0a1.en-US.mac.txt']
os = "mac"
build_examples = ["firefox-38.0a1.en-US.mac.dmg"]
build_info_examples = ["firefox-38.0a1.en-US.mac.txt"]
class TestThunderbirdConfig(unittest.TestCase):
os = 'linux'
os = "linux"
bits = 64
processor = 'x86_64'
processor = "x86_64"
def setUp(self):
self.conf = create_config('thunderbird', self.os, self.bits,
self.processor)
self.conf = create_config("thunderbird", self.os, self.bits, self.processor)
def test_nightly_repo_regex_before_2008_07_26(self):
repo_regex = self.conf.get_nightly_repo_regex(datetime.date(2008,
7, 25))
self.assertEqual(repo_regex, '^2008-07-25-[\\d-]+trunk/$')
repo_regex = self.conf.get_nightly_repo_regex(datetime.date(2008, 7, 25))
self.assertEqual(repo_regex, "^2008-07-25-[\\d-]+trunk/$")
def test_nightly_repo_regex_before_2009_01_09(self):
repo_regex = self.conf.get_nightly_repo_regex(datetime.date(2009,
1, 8))
self.assertEqual(repo_regex, '^2009-01-08-[\\d-]+comm-central/$')
repo_regex = self.conf.get_nightly_repo_regex(datetime.date(2009, 1, 8))
self.assertEqual(repo_regex, "^2009-01-08-[\\d-]+comm-central/$")
def test_nightly_repo_regex_before_2010_08_21(self):
repo_regex = self.conf.get_nightly_repo_regex(datetime.date(2010,
8, 20))
self.assertEqual(repo_regex, '^2010-08-20-[\\d-]+comm-central-trunk/$')
repo_regex = self.conf.get_nightly_repo_regex(datetime.date(2010, 8, 20))
self.assertEqual(repo_regex, "^2010-08-20-[\\d-]+comm-central-trunk/$")
def test_nightly_repo_regex_since_2010_08_21(self):
repo_regex = self.conf.get_nightly_repo_regex(datetime.date(2010,
8, 21))
self.assertEqual(repo_regex, '^2010-08-21-[\\d-]+comm-central/$')
repo_regex = self.conf.get_nightly_repo_regex(datetime.date(2010, 8, 21))
self.assertEqual(repo_regex, "^2010-08-21-[\\d-]+comm-central/$")
class TestThunderbirdConfigWin(TestThunderbirdConfig):
os = 'win'
os = "win"
def test_nightly_repo_regex_before_2008_07_26(self):
with self.assertRaises(errors.WinTooOldBuildError):
TestThunderbirdConfig.\
test_nightly_repo_regex_before_2008_07_26(self)
TestThunderbirdConfig.test_nightly_repo_regex_before_2008_07_26(self)
def test_nightly_repo_regex_before_2009_01_09(self):
with self.assertRaises(errors.WinTooOldBuildError):
TestThunderbirdConfig.\
test_nightly_repo_regex_before_2009_01_09(self)
TestThunderbirdConfig.test_nightly_repo_regex_before_2009_01_09(self)
class TestFennecConfig(unittest.TestCase):
def setUp(self):
self.conf = create_config('fennec', 'linux', 64, None)
self.conf = create_config("fennec", "linux", 64, None)
def test_get_nightly_repo_regex(self):
regex = self.conf.get_nightly_repo_regex(datetime.date(2014,
12, 5))
regex = self.conf.get_nightly_repo_regex(datetime.date(2014, 12, 5))
self.assertIn("mozilla-central-android", regex)
regex = self.conf.get_nightly_repo_regex(datetime.date(2014,
12, 10))
regex = self.conf.get_nightly_repo_regex(datetime.date(2014, 12, 10))
self.assertIn("mozilla-central-android-api-10", regex)
regex = self.conf.get_nightly_repo_regex(datetime.date(2015,
1, 1))
regex = self.conf.get_nightly_repo_regex(datetime.date(2015, 1, 1))
self.assertIn("mozilla-central-android-api-11", regex)
regex = self.conf.get_nightly_repo_regex(datetime.date(2016,
1, 28))
regex = self.conf.get_nightly_repo_regex(datetime.date(2016, 1, 28))
self.assertIn("mozilla-central-android-api-11", regex)
regex = self.conf.get_nightly_repo_regex(datetime.date(2016,
1, 29))
regex = self.conf.get_nightly_repo_regex(datetime.date(2016, 1, 29))
self.assertIn("mozilla-central-android-api-15", regex)
regex = self.conf.get_nightly_repo_regex(datetime.date(2017,
8, 30))
regex = self.conf.get_nightly_repo_regex(datetime.date(2017, 8, 30))
self.assertIn("mozilla-central-android-api-16", regex)
def test_build_regex(self):
regex = re.compile(self.conf.build_regex())
self.assertTrue(regex.match('fennec-36.0a1.multi.android-arm.apk'))
self.assertTrue(regex.match("fennec-36.0a1.multi.android-arm.apk"))
def test_build_info_regex(self):
regex = re.compile(self.conf.build_info_regex())
self.assertTrue(regex.match('fennec-36.0a1.multi.android-arm.txt'))
self.assertTrue(regex.match("fennec-36.0a1.multi.android-arm.txt"))
class TestFennec23Config(unittest.TestCase):
def setUp(self):
self.conf = create_config('fennec-2.3', 'linux', 64, None)
self.conf = create_config("fennec-2.3", "linux", 64, None)
def test_class_attr_name(self):
self.assertEqual(self.conf.app_name, 'fennec')
self.assertEqual(self.conf.app_name, "fennec")
def test_get_nightly_repo_regex(self):
regex = self.conf.get_nightly_repo_regex(datetime.date(2014, 12, 5))
@ -210,127 +194,203 @@ class TestFennec23Config(unittest.TestCase):
class TestGetBuildUrl(unittest.TestCase):
def test_for_linux(self):
self.assertEqual(get_build_regex('test', 'linux', 32, 'x86'),
r'(target|test.*linux-i686)\.tar.bz2')
self.assertEqual(
get_build_regex("test", "linux", 32, "x86"), r"(target|test.*linux-i686)\.tar.bz2",
)
self.assertEqual(get_build_regex('test', 'linux', 64, 'x86_64'),
r'(target|test.*linux-x86_64)\.tar.bz2')
self.assertEqual(
get_build_regex("test", "linux", 64, "x86_64"), r"(target|test.*linux-x86_64)\.tar.bz2",
)
self.assertEqual(get_build_regex('test', 'linux', 64, 'x86_64',
with_ext=False),
r'(target|test.*linux-x86_64)')
self.assertEqual(
get_build_regex("test", "linux", 64, "x86_64", with_ext=False),
r"(target|test.*linux-x86_64)",
)
def test_for_win(self):
self.assertEqual(get_build_regex('test', 'win', 32, 'x86'),
r'(target|test.*win32)\.zip')
self.assertEqual(get_build_regex('test', 'win', 64, 'x86_64'),
r'(target|test.*win64(-x86_64)?)\.zip')
self.assertEqual(get_build_regex('test', 'win', 64, 'x86_64',
with_ext=False),
r'(target|test.*win64(-x86_64)?)')
self.assertEqual(get_build_regex('test', 'win', 32, 'aarch64'),
r'(target|test.*win32)\.zip')
self.assertEqual(get_build_regex('test', 'win', 64, 'aarch64'),
r'(target|test.*win64-aarch64)\.zip')
self.assertEqual(get_build_regex("test", "win", 32, "x86"), r"(target|test.*win32)\.zip")
self.assertEqual(
get_build_regex("test", "win", 64, "x86_64"), r"(target|test.*win64(-x86_64)?)\.zip",
)
self.assertEqual(
get_build_regex("test", "win", 64, "x86_64", with_ext=False),
r"(target|test.*win64(-x86_64)?)",
)
self.assertEqual(
get_build_regex("test", "win", 32, "aarch64"), r"(target|test.*win32)\.zip"
)
self.assertEqual(
get_build_regex("test", "win", 64, "aarch64"), r"(target|test.*win64-aarch64)\.zip",
)
def test_for_mac(self):
self.assertEqual(get_build_regex('test', 'mac', 32, 'x86'),
r'(target|test.*mac.*)\.dmg')
self.assertEqual(get_build_regex('test', 'mac', 64, 'x86_64'),
r'(target|test.*mac.*)\.dmg')
self.assertEqual(get_build_regex('test', 'mac', 64, 'x86_64',
with_ext=False),
r'(target|test.*mac.*)')
self.assertEqual(get_build_regex("test", "mac", 32, "x86"), r"(target|test.*mac.*)\.dmg")
self.assertEqual(get_build_regex("test", "mac", 64, "x86_64"), r"(target|test.*mac.*)\.dmg")
self.assertEqual(
get_build_regex("test", "mac", 64, "x86_64", with_ext=False), r"(target|test.*mac.*)",
)
def test_unknown_os(self):
with self.assertRaises(errors.MozRegressionError):
get_build_regex('test', 'unknown', 32, 'x86')
get_build_regex("test", "unknown", 32, "x86")
class TestFallbacksConfig(TestFirefoxConfigLinux64):
def setUp(self):
self.conf = create_config(self.app_name, self.os, self.bits,
self.processor)
self.conf.BUILD_TYPE_FALLBACKS = {
'opt': ('test', 'fallback')
}
self.conf.set_build_type('opt')
self.conf = create_config(self.app_name, self.os, self.bits, self.processor)
self.conf.BUILD_TYPE_FALLBACKS = {"opt": ("test", "fallback")}
self.conf.set_build_type("opt")
def test_fallbacking(self):
assert self.conf.build_type == 'opt'
assert self.conf.build_type == "opt"
self.conf._inc_used_build()
assert self.conf.build_type == 'test'
assert self.conf.build_type == "test"
self.conf._inc_used_build()
assert self.conf.build_type == 'fallback'
assert self.conf.build_type == "fallback"
# Check we wrap
self.conf._inc_used_build()
assert self.conf.build_type == 'opt'
assert self.conf.build_type == "opt"
def test_fallback_routes(self):
routes = list(self.conf.tk_routes(
create_push('1a', TIMESTAMP_TEST)
))
routes = list(self.conf.tk_routes(create_push("1a", TIMESTAMP_TEST)))
assert len(routes) == 3
assert routes[0] == (
'gecko.v2.mozilla-central.revision.1a.firefox.linux64-opt'
)
assert routes[2] == (
'gecko.v2.mozilla-central.revision.1a.firefox.linux64-fallback'
)
assert routes[0] == ("gecko.v2.mozilla-central.revision.1a.firefox.linux64-opt")
assert routes[2] == ("gecko.v2.mozilla-central.revision.1a.firefox.linux64-fallback")
class TestAarch64AvailableBuildTypes(unittest.TestCase):
app_name = 'firefox'
os = 'win'
app_name = "firefox"
os = "win"
bits = 64
processor = 'aarch64'
processor = "aarch64"
def setUp(self):
self.conf = create_config(self.app_name, self.os, self.bits,
self.processor)
self.conf = create_config(self.app_name, self.os, self.bits, self.processor)
self.conf.BUILD_TYPES = (
'excluded[win64]', 'included[win64-aarch64]', 'default'
"excluded[win64]",
"included[win64-aarch64]",
"default",
)
def test_aarch64_build_types(self):
build_types = self.conf.available_build_types()
assert len(build_types) == 2
assert 'default' in build_types
assert 'included' in build_types
assert 'excluded' not in build_types
assert "default" in build_types
assert "included" in build_types
assert "excluded" not in build_types
CHSET = "47856a21491834da3ab9b308145caa8ec1b98ee1"
CHSET12 = "47856a214918"
@pytest.mark.parametrize("app,os,bits,processor,repo,push_date,expected", [
# firefox
("firefox", 'win', 64, 'aarch64', 'm-i', TIMESTAMP_TEST,
'gecko.v2.mozilla-inbound.shippable.revision.%s.firefox.win64-aarch64-opt' % CHSET),
("firefox", 'win', 32, 'aarch64', 'm-i', TIMESTAMP_TEST,
'gecko.v2.mozilla-inbound.shippable.revision.%s.firefox.win32-opt' % CHSET),
("firefox", 'mac', 64, 'x86_64', 'm-i', TIMESTAMP_TEST,
'gecko.v2.mozilla-inbound.shippable.revision.%s.firefox.macosx64-opt' % CHSET),
("firefox", 'linux', 64, 'x86_64', 'm-i', TIMESTAMP_TEST,
'gecko.v2.mozilla-inbound.shippable.revision.%s.firefox.linux64-opt' % CHSET),
("firefox", 'linux', 64, 'x86_64', 'try', TIMESTAMP_TEST,
'gecko.v2.try.shippable.revision.%s.firefox.linux64-opt' % CHSET),
# fennec
("fennec", None, None, None, None, TIMESTAMP_FENNEC_API_15 - 1,
'gecko.v2.mozilla-central.revision.%s.mobile.android-api-11-opt' % CHSET),
("fennec", None, None, None, None, TIMESTAMP_FENNEC_API_15,
'gecko.v2.mozilla-central.revision.%s.mobile.android-api-15-opt' % CHSET),
("fennec", None, None, None, None, TIMESTAMP_FENNEC_API_16,
'gecko.v2.mozilla-central.revision.%s.mobile.android-api-16-opt' % CHSET),
("fennec-2.3", None, None, None, 'm-i', TIMESTAMP_TEST,
'gecko.v2.mozilla-inbound.revision.%s.mobile.android-api-9-opt' % CHSET),
# thunderbird
('thunderbird', 'win', 32, 'x86_64', 'comm-central', TIMESTAMP_TEST,
'comm.v2.comm-central.revision.%s.thunderbird.win32-opt' % CHSET),
('thunderbird', 'linux', 64, 'x86_64', 'comm-beta', TIMESTAMP_TEST,
'comm.v2.comm-beta.revision.%s.thunderbird.linux64-opt' % CHSET),
])
@pytest.mark.parametrize(
"app,os,bits,processor,repo,push_date,expected",
[
# firefox
(
"firefox",
"win",
64,
"aarch64",
"m-i",
TIMESTAMP_TEST,
"gecko.v2.mozilla-inbound.shippable.revision.%s.firefox.win64-aarch64-opt" % CHSET,
),
(
"firefox",
"win",
32,
"aarch64",
"m-i",
TIMESTAMP_TEST,
"gecko.v2.mozilla-inbound.shippable.revision.%s.firefox.win32-opt" % CHSET,
),
(
"firefox",
"mac",
64,
"x86_64",
"m-i",
TIMESTAMP_TEST,
"gecko.v2.mozilla-inbound.shippable.revision.%s.firefox.macosx64-opt" % CHSET,
),
(
"firefox",
"linux",
64,
"x86_64",
"m-i",
TIMESTAMP_TEST,
"gecko.v2.mozilla-inbound.shippable.revision.%s.firefox.linux64-opt" % CHSET,
),
(
"firefox",
"linux",
64,
"x86_64",
"try",
TIMESTAMP_TEST,
"gecko.v2.try.shippable.revision.%s.firefox.linux64-opt" % CHSET,
),
# fennec
(
"fennec",
None,
None,
None,
None,
TIMESTAMP_FENNEC_API_15 - 1,
"gecko.v2.mozilla-central.revision.%s.mobile.android-api-11-opt" % CHSET,
),
(
"fennec",
None,
None,
None,
None,
TIMESTAMP_FENNEC_API_15,
"gecko.v2.mozilla-central.revision.%s.mobile.android-api-15-opt" % CHSET,
),
(
"fennec",
None,
None,
None,
None,
TIMESTAMP_FENNEC_API_16,
"gecko.v2.mozilla-central.revision.%s.mobile.android-api-16-opt" % CHSET,
),
(
"fennec-2.3",
None,
None,
None,
"m-i",
TIMESTAMP_TEST,
"gecko.v2.mozilla-inbound.revision.%s.mobile.android-api-9-opt" % CHSET,
),
# thunderbird
(
"thunderbird",
"win",
32,
"x86_64",
"comm-central",
TIMESTAMP_TEST,
"comm.v2.comm-central.revision.%s.thunderbird.win32-opt" % CHSET,
),
(
"thunderbird",
"linux",
64,
"x86_64",
"comm-beta",
TIMESTAMP_TEST,
"comm.v2.comm-beta.revision.%s.thunderbird.linux64-opt" % CHSET,
),
],
)
def test_tk_route(app, os, bits, processor, repo, push_date, expected):
conf = create_config(app, os, bits, processor)
conf.set_repo(repo)
@ -338,80 +398,95 @@ def test_tk_route(app, os, bits, processor, repo, push_date, expected):
assert result == expected
@pytest.mark.parametrize("app,os,bits,processor,build_type,expected", [
# firefox
("firefox", 'linux', 64, 'x86_64', "asan",
'gecko.v2.mozilla-central.revision.%s.firefox.linux64-asan' % CHSET),
("firefox", 'linux', 64, 'x86_64', 'shippable',
'gecko.v2.mozilla-central.shippable.revision.%s.firefox.linux64-opt'
% CHSET),
])
def test_tk_route_with_build_type(app, os, bits, processor, build_type,
expected):
@pytest.mark.parametrize(
"app,os,bits,processor,build_type,expected",
[
# firefox
(
"firefox",
"linux",
64,
"x86_64",
"asan",
"gecko.v2.mozilla-central.revision.%s.firefox.linux64-asan" % CHSET,
),
(
"firefox",
"linux",
64,
"x86_64",
"shippable",
"gecko.v2.mozilla-central.shippable.revision.%s.firefox.linux64-opt" % CHSET,
),
],
)
def test_tk_route_with_build_type(app, os, bits, processor, build_type, expected):
conf = create_config(app, os, bits, processor)
conf.set_build_type(build_type)
result = conf.tk_route(
create_push(CHSET, TIMESTAMP_TEST))
result = conf.tk_route(create_push(CHSET, TIMESTAMP_TEST))
assert result == expected
def test_set_build_type():
conf = create_config('firefox', 'linux', 64, 'x86_64')
assert conf.build_type == 'shippable' # desktop Fx default is shippable
conf.set_build_type('debug')
assert conf.build_type == 'debug'
conf = create_config("firefox", "linux", 64, "x86_64")
assert conf.build_type == "shippable" # desktop Fx default is shippable
conf.set_build_type("debug")
assert conf.build_type == "debug"
def test_set_bad_build_type():
conf = create_config('firefox', 'linux', 64, 'x86_64')
conf = create_config("firefox", "linux", 64, "x86_64")
with pytest.raises(errors.MozRegressionError):
conf.set_build_type("wrong build type")
def test_jsshell_build_info_regex():
conf = create_config('jsshell', 'linux', 64, 'x86_64')
assert re.match(conf.build_info_regex(),
'firefox-38.0a1.en-US.linux-x86_64.txt')
conf = create_config("jsshell", "linux", 64, "x86_64")
assert re.match(conf.build_info_regex(), "firefox-38.0a1.en-US.linux-x86_64.txt")
@pytest.mark.parametrize('os,bits,processor,name', [
('linux', 32, 'x86', 'jsshell-linux-i686.zip'),
('linux', 64, 'x86_64', 'jsshell-linux-x86_64.zip'),
('mac', 64, 'x86_64', 'jsshell-mac.zip'),
('win', 32, 'x86', 'jsshell-win32.zip'),
('win', 64, 'x86_64', 'jsshell-win64.zip'),
('win', 64, 'x86_64', 'jsshell-win64-x86_64.zip'),
('win', 32, 'aarch64', 'jsshell-win32.zip'),
('win', 64, 'aarch64', 'jsshell-win64-aarch64.zip'),
])
@pytest.mark.parametrize(
"os,bits,processor,name",
[
("linux", 32, "x86", "jsshell-linux-i686.zip"),
("linux", 64, "x86_64", "jsshell-linux-x86_64.zip"),
("mac", 64, "x86_64", "jsshell-mac.zip"),
("win", 32, "x86", "jsshell-win32.zip"),
("win", 64, "x86_64", "jsshell-win64.zip"),
("win", 64, "x86_64", "jsshell-win64-x86_64.zip"),
("win", 32, "aarch64", "jsshell-win32.zip"),
("win", 64, "aarch64", "jsshell-win64-aarch64.zip"),
],
)
def test_jsshell_build_regex(os, bits, processor, name):
conf = create_config('jsshell', os, bits, processor)
conf = create_config("jsshell", os, bits, processor)
assert re.match(conf.build_regex(), name)
def test_jsshell_x86_64_build_regex():
conf = create_config('jsshell', 'win', 64, 'x86_64')
assert not re.match(conf.build_regex(), 'jsshell-win64-aarch64.zip')
conf = create_config("jsshell", "win", 64, "x86_64")
assert not re.match(conf.build_regex(), "jsshell-win64-aarch64.zip")
@pytest.mark.parametrize('os,bits,processor,tc_suffix', [
('linux', 32, 'x86', 'linux-pgo'),
('linux', 64, 'x86_64', 'linux64-pgo'),
('mac', 64, 'x86_64', errors.MozRegressionError),
('win', 32, 'x86', 'win32-pgo'),
('win', 64, 'x86_64', 'win64-pgo'),
])
@pytest.mark.parametrize(
"os,bits,processor,tc_suffix",
[
("linux", 32, "x86", "linux-pgo"),
("linux", 64, "x86_64", "linux64-pgo"),
("mac", 64, "x86_64", errors.MozRegressionError),
("win", 32, "x86", "win32-pgo"),
("win", 64, "x86_64", "win64-pgo"),
],
)
def test_set_firefox_build_type_pgo(os, bits, processor, tc_suffix):
conf = create_config('firefox', os, bits, processor)
conf = create_config("firefox", os, bits, processor)
if type(tc_suffix) is not str:
with pytest.raises(tc_suffix):
conf.set_build_type('pgo')
conf.set_build_type("pgo")
else:
conf.set_build_type('pgo')
assert conf.tk_route(
create_push(CHSET, TIMESTAMP_TEST)) \
.endswith('.' + tc_suffix)
conf.set_build_type("pgo")
assert conf.tk_route(create_push(CHSET, TIMESTAMP_TEST)).endswith("." + tc_suffix)
if __name__ == '__main__':
if __name__ == "__main__":
unittest.main()

Просмотреть файл

@ -5,110 +5,106 @@ from datetime import date, datetime
import pytest
from mock import Mock, call
from mozregression.errors import EmptyPushlogError, MozRegressionError
from mozregression.json_pushes import JsonPushes, Push
from mozregression.errors import MozRegressionError, EmptyPushlogError
def test_push(mocker):
pushlog = {'1': {
'changesets': ['a', 'b', 'c'],
'date': 123456,
}}
retry_get = mocker.patch('mozregression.json_pushes.retry_get')
pushlog = {"1": {"changesets": ["a", "b", "c"], "date": 123456}}
retry_get = mocker.patch("mozregression.json_pushes.retry_get")
response = Mock(json=Mock(return_value=pushlog))
retry_get.return_value = response
jpushes = JsonPushes()
push = jpushes.push('validchangeset')
push = jpushes.push("validchangeset")
assert isinstance(push, Push)
assert push.push_id == '1'
assert push.changeset == 'c'
assert push.changesets[0] == 'a'
assert push.push_id == "1"
assert push.changeset == "c"
assert push.changesets[0] == "a"
assert push.timestamp == 123456
assert push.utc_date == datetime(1970, 1, 2, 10, 17, 36)
assert str(push) == 'c'
assert str(push) == "c"
retry_get.assert_called_once_with(
'https://hg.mozilla.org/mozilla-central/json-pushes'
'?changeset=validchangeset'
"https://hg.mozilla.org/mozilla-central/json-pushes" "?changeset=validchangeset"
)
def test_push_404_error(mocker):
retry_get = mocker.patch('mozregression.json_pushes.retry_get')
retry_get = mocker.patch("mozregression.json_pushes.retry_get")
response = Mock(status_code=404, json=Mock(return_value={"error": "unknown revision"}))
retry_get.return_value = response
jpushes = JsonPushes()
with pytest.raises(MozRegressionError):
jpushes.push('invalid_changeset')
jpushes.push("invalid_changeset")
def test_push_nothing_found(mocker):
retry_get = mocker.patch('mozregression.json_pushes.retry_get')
retry_get = mocker.patch("mozregression.json_pushes.retry_get")
response = Mock(json=Mock(return_value={}))
retry_get.return_value = response
jpushes = JsonPushes()
with pytest.raises(MozRegressionError):
jpushes.push('invalid_changeset')
jpushes.push("invalid_changeset")
@pytest.mark.skipif(sys.version_info < (3, 0), reason="fails on python 2 for some unknown reason")
def test_pushes_within_changes(mocker):
push_first = {'1': {'changesets': ['a']}}
other_pushes = {
'2': {'changesets': ['b']},
'3': {'changesets': ['c']}
}
push_first = {"1": {"changesets": ["a"]}}
other_pushes = {"2": {"changesets": ["b"]}, "3": {"changesets": ["c"]}}
retry_get = mocker.patch('mozregression.json_pushes.retry_get')
retry_get = mocker.patch("mozregression.json_pushes.retry_get")
response = Mock(json=Mock(side_effect=[push_first, other_pushes]))
retry_get.return_value = response
jpushes = JsonPushes()
pushes = jpushes.pushes_within_changes('fromchset', "tochset")
pushes = jpushes.pushes_within_changes("fromchset", "tochset")
assert pushes[0].push_id == '1'
assert pushes[0].changeset == 'a'
assert pushes[1].push_id == '2'
assert pushes[1].changeset == 'b'
assert pushes[2].push_id == '3'
assert pushes[2].changeset == 'c'
assert pushes[0].push_id == "1"
assert pushes[0].changeset == "a"
assert pushes[1].push_id == "2"
assert pushes[1].changeset == "b"
assert pushes[2].push_id == "3"
assert pushes[2].changeset == "c"
retry_get.assert_has_calls([
call('https://hg.mozilla.org/mozilla-central/json-pushes'
'?changeset=fromchset'),
call('https://hg.mozilla.org/mozilla-central/json-pushes'
'?fromchange=fromchset&tochange=tochset'),
])
retry_get.assert_has_calls(
[
call("https://hg.mozilla.org/mozilla-central/json-pushes" "?changeset=fromchset"),
call(
"https://hg.mozilla.org/mozilla-central/json-pushes"
"?fromchange=fromchset&tochange=tochset"
),
]
)
def test_pushes_within_changes_using_dates(mocker):
p1 = {'changesets': ['abc'], 'date': 12345}
p2 = {'changesets': ['def'], 'date': 67891}
pushes = {'1': p1, '2': p2}
p1 = {"changesets": ["abc"], "date": 12345}
p2 = {"changesets": ["def"], "date": 67891}
pushes = {"1": p1, "2": p2}
retry_get = mocker.patch('mozregression.json_pushes.retry_get')
retry_get = mocker.patch("mozregression.json_pushes.retry_get")
retry_get.return_value = Mock(json=Mock(return_value=pushes))
jpushes = JsonPushes(branch='m-i')
jpushes = JsonPushes(branch="m-i")
pushes = jpushes.pushes_within_changes(date(2015, 1, 1), date(2015, 2, 2))
assert pushes[0].push_id == '1'
assert pushes[1].push_id == '2'
assert pushes[0].push_id == "1"
assert pushes[1].push_id == "2"
retry_get.assert_called_once_with(
'https://hg.mozilla.org/integration/mozilla-inbound/json-pushes?'
'enddate=2015-02-03&startdate=2015-01-01'
"https://hg.mozilla.org/integration/mozilla-inbound/json-pushes?"
"enddate=2015-02-03&startdate=2015-01-01"
)
def test_push_with_date_raise_appropriate_error():
jpushes = JsonPushes(branch='inbound')
jpushes = JsonPushes(branch="inbound")
jpushes.pushes_within_changes = Mock(side_effect=EmptyPushlogError)
with pytest.raises(EmptyPushlogError) as ctx:
jpushes.push(date(2015, 1, 1))
assert str(ctx.value) == \
'No pushes available for the date 2015-01-01 on inbound.'
assert str(ctx.value) == "No pushes available for the date 2015-01-01 on inbound."

Просмотреть файл

@ -1,17 +1,19 @@
from __future__ import absolute_import
from mozregression import launchers
import unittest
import os
import pytest
import tempfile
import unittest
import mozfile
import mozinfo
import mozversion
from mock import patch, Mock, ANY
from mozprofile import Profile
from mozregression.errors import LauncherNotRunnable, LauncherError
import pytest
from mock import ANY, Mock, patch
from mozdevice import ADBError
from mozprofile import Profile
from mozregression import launchers
from mozregression.errors import LauncherError, LauncherNotRunnable
class MyLauncher(launchers.Launcher):
@ -33,9 +35,8 @@ class MyLauncher(launchers.Launcher):
class TestLauncher(unittest.TestCase):
def test_start_stop(self):
launcher = MyLauncher('/foo/persist.zip')
launcher = MyLauncher("/foo/persist.zip")
self.assertFalse(launcher.started)
launcher.start()
# now it has been started
@ -50,7 +51,7 @@ class TestLauncher(unittest.TestCase):
self.assertTrue(launcher.started)
def test_wait(self):
launcher = MyLauncher('/foo/persist.zip')
launcher = MyLauncher("/foo/persist.zip")
self.assertFalse(launcher.started)
launcher.start()
# now it has been started
@ -65,17 +66,17 @@ class TestLauncher(unittest.TestCase):
raise Exception()
with self.assertRaises(LauncherError):
FailingLauncher('/foo/persist.zip')
FailingLauncher("/foo/persist.zip")
def test_start_fail(self):
launcher = MyLauncher('/foo/persist.zip')
launcher = MyLauncher("/foo/persist.zip")
launcher._start = Mock(side_effect=Exception)
with self.assertRaises(LauncherError):
launcher.start()
def test_stop_fail(self):
launcher = MyLauncher('/foo/persist.zip')
launcher = MyLauncher("/foo/persist.zip")
launcher._stop = Mock(side_effect=Exception)
launcher.start()
@ -84,36 +85,39 @@ class TestLauncher(unittest.TestCase):
class TestMozRunnerLauncher(unittest.TestCase):
@patch('mozregression.launchers.mozinstall')
@patch("mozregression.launchers.mozinstall")
def setUp(self, mozinstall):
mozinstall.get_binary.return_value = '/binary'
self.launcher = launchers.MozRunnerLauncher('/binary')
mozinstall.get_binary.return_value = "/binary"
self.launcher = launchers.MozRunnerLauncher("/binary")
# patch profile_class else we will have some temporary dirs not deleted
@patch('mozregression.launchers.MozRunnerLauncher.\
profile_class', spec=Profile)
@patch(
"mozregression.launchers.MozRunnerLauncher.\
profile_class",
spec=Profile,
)
def launcher_start(self, profile_class, *args, **kwargs):
self.profile_class = profile_class
self.launcher.start(*args, **kwargs)
def test_installed(self):
with self.launcher:
self.assertEqual(self.launcher.binary, '/binary')
self.assertEqual(self.launcher.binary, "/binary")
@patch('mozregression.launchers.Runner')
@patch("mozregression.launchers.Runner")
def test_start_no_args(self, Runner):
with self.launcher:
self.launcher_start()
kwargs = Runner.call_args[1]
self.assertEqual(kwargs['cmdargs'], ())
self.assertEqual(kwargs['binary'], '/binary')
self.assertIsInstance(kwargs['profile'], Profile)
self.assertEqual(kwargs["cmdargs"], ())
self.assertEqual(kwargs["binary"], "/binary")
self.assertIsInstance(kwargs["profile"], Profile)
# runner is started
self.launcher.runner.start.assert_called_once_with()
self.launcher.stop()
@patch('mozregression.launchers.Runner')
@patch("mozregression.launchers.Runner")
def test_wait(self, Runner):
runner = Mock(wait=Mock(return_value=0))
Runner.return_value = runner
@ -122,42 +126,43 @@ profile_class', spec=Profile)
self.assertEqual(self.launcher.wait(), 0)
runner.wait.assert_called_once_with()
@patch('mozregression.launchers.Runner')
@patch("mozregression.launchers.Runner")
def test_start_with_addons(self, Runner):
with self.launcher:
self.launcher_start(addons=['my-addon'], preferences='my-prefs')
self.profile_class.assert_called_once_with(addons=['my-addon'],
preferences='my-prefs')
self.launcher_start(addons=["my-addon"], preferences="my-prefs")
self.profile_class.assert_called_once_with(addons=["my-addon"], preferences="my-prefs")
# runner is started
self.launcher.runner.start.assert_called_once_with()
self.launcher.stop()
@patch('mozregression.launchers.Runner')
@patch("mozregression.launchers.Runner")
def test_start_with_profile_and_addons(self, Runner):
temp_dir_profile = tempfile.mkdtemp()
self.addCleanup(mozfile.remove, temp_dir_profile)
with self.launcher:
self.launcher_start(profile=temp_dir_profile, addons=['my-addon'],
preferences='my-prefs')
self.launcher_start(
profile=temp_dir_profile, addons=["my-addon"], preferences="my-prefs"
)
self.profile_class.clone.assert_called_once_with(
temp_dir_profile, addons=['my-addon'], preferences='my-prefs')
temp_dir_profile, addons=["my-addon"], preferences="my-prefs"
)
# runner is started
self.launcher.runner.start.assert_called_once_with()
self.launcher.stop()
@patch('mozregression.launchers.Runner')
@patch('mozregression.launchers.mozversion')
@patch("mozregression.launchers.Runner")
@patch("mozregression.launchers.mozversion")
def test_get_app_infos(self, mozversion, Runner):
mozversion.get_version.return_value = {'some': 'infos'}
mozversion.get_version.return_value = {"some": "infos"}
with self.launcher:
self.launcher_start()
self.assertEqual(self.launcher.get_app_info(), {'some': 'infos'})
mozversion.get_version.assert_called_once_with(binary='/binary')
self.assertEqual(self.launcher.get_app_info(), {"some": "infos"})
mozversion.get_version.assert_called_once_with(binary="/binary")
self.launcher.stop()
@patch('mozregression.launchers.Runner')
@patch('mozversion.get_version')
@patch("mozregression.launchers.Runner")
@patch("mozversion.get_version")
def test_get_app_infos_error(self, get_version, Runner):
get_version.side_effect = mozversion.VersionError("err")
with self.launcher:
@ -174,97 +179,89 @@ profile_class', spec=Profile)
def test_firefox_install(mocker):
install_ext, binary_name = (
('zip', 'firefox.exe') if mozinfo.isWin else
('tar.bz2', 'firefox') if mozinfo.isLinux else
('dmg', 'firefox') # if mozinfo.ismac
("zip", "firefox.exe")
if mozinfo.isWin
else ("tar.bz2", "firefox")
if mozinfo.isLinux
else ("dmg", "firefox") # if mozinfo.ismac
)
installer_file = 'firefox.{}'.format(install_ext)
installer_file = "firefox.{}".format(install_ext)
installer = os.path.abspath(
os.path.join('tests', 'unit', 'installer_stubs', installer_file)
)
installer = os.path.abspath(os.path.join("tests", "unit", "installer_stubs", installer_file))
assert os.path.isfile(installer)
with launchers.FirefoxLauncher(installer) as fx:
assert os.path.isdir(fx.tempdir)
assert os.path.basename(fx.binary) == binary_name
installdir = os.path.dirname(fx.binary)
if mozinfo.isMac:
installdir = os.path.normpath(
os.path.join(installdir, '..', 'Resources')
)
assert os.path.exists(os.path.join(installdir, 'distribution', 'policies.json'))
installdir = os.path.normpath(os.path.join(installdir, "..", "Resources"))
assert os.path.exists(os.path.join(installdir, "distribution", "policies.json"))
assert not os.path.isdir(fx.tempdir)
class TestFennecLauncher(unittest.TestCase):
test_root = '/sdcard/tmp'
test_root = "/sdcard/tmp"
def setUp(self):
self.profile = Profile()
self.addCleanup(self.profile.cleanup)
self.remote_profile_path = self.test_root + \
'/' + os.path.basename(self.profile.profile)
self.remote_profile_path = self.test_root + "/" + os.path.basename(self.profile.profile)
@patch('mozregression.launchers.mozversion.get_version')
@patch('mozregression.launchers.ADBAndroid')
@patch("mozregression.launchers.mozversion.get_version")
@patch("mozregression.launchers.ADBAndroid")
def create_launcher(self, ADBAndroid, get_version, **kwargs):
self.adb = Mock(test_root=self.test_root)
if kwargs.get('uninstall_error'):
if kwargs.get("uninstall_error"):
self.adb.uninstall_app.side_effect = launchers.ADBError
ADBAndroid.return_value = self.adb
get_version.return_value = kwargs.get('version_value', {})
return launchers.FennecLauncher('/binary')
get_version.return_value = kwargs.get("version_value", {})
return launchers.FennecLauncher("/binary")
def test_install(self):
self.create_launcher()
self.adb.uninstall_app.assert_called_with("org.mozilla.fennec")
self.adb.install_app.assert_called_with('/binary')
self.adb.install_app.assert_called_with("/binary")
@patch('mozregression.launchers.FennecLauncher._create_profile')
@patch("mozregression.launchers.FennecLauncher._create_profile")
def test_start_stop(self, _create_profile):
# Force use of existing profile
_create_profile.return_value = self.profile
launcher = self.create_launcher()
launcher.start(profile='my_profile')
launcher.start(profile="my_profile")
self.adb.exists.assert_called_once_with(self.remote_profile_path)
self.adb.rm.assert_called_once_with(self.remote_profile_path,
recursive=True)
self.adb.push.assert_called_once_with(self.profile.profile,
self.remote_profile_path)
self.adb.rm.assert_called_once_with(self.remote_profile_path, recursive=True)
self.adb.push.assert_called_once_with(self.profile.profile, self.remote_profile_path)
self.adb.launch_fennec.assert_called_once_with(
"org.mozilla.fennec",
extra_args=['-profile', self.remote_profile_path]
"org.mozilla.fennec", extra_args=["-profile", self.remote_profile_path]
)
# ensure get_app_info returns something
self.assertIsNotNone(launcher.get_app_info())
launcher.stop()
self.adb.stop_application.assert_called_once_with("org.mozilla.fennec")
@patch('mozregression.launchers.FennecLauncher._create_profile')
@patch("mozregression.launchers.FennecLauncher._create_profile")
def test_adb_calls_with_custom_package_name(self, _create_profile):
# Force use of existing profile
_create_profile.return_value = self.profile
pkg_name = 'org.mozilla.custom'
launcher = \
self.create_launcher(version_value={'package_name': pkg_name})
pkg_name = "org.mozilla.custom"
launcher = self.create_launcher(version_value={"package_name": pkg_name})
self.adb.uninstall_app.assert_called_once_with(pkg_name)
launcher.start(profile='my_profile')
launcher.start(profile="my_profile")
self.adb.launch_fennec.assert_called_once_with(
pkg_name,
extra_args=['-profile', self.remote_profile_path]
pkg_name, extra_args=["-profile", self.remote_profile_path]
)
launcher.stop()
self.adb.stop_application.assert_called_once_with(pkg_name)
@patch('mozregression.launchers.LOG')
@patch("mozregression.launchers.LOG")
def test_adb_first_uninstall_fail(self, log):
self.create_launcher(uninstall_error=True)
log.warning.assert_called_once_with(ANY)
self.adb.install_app.assert_called_once_with(ANY)
@patch('mozregression.launchers.ADBHost')
@patch("mozregression.launchers.ADBHost")
def test_check_is_runnable(self, ADBHost):
devices = Mock(return_value=True)
ADBHost.return_value = Mock(devices=devices)
@ -273,16 +270,14 @@ class TestFennecLauncher(unittest.TestCase):
# exception raised if there is no device
devices.return_value = False
self.assertRaises(LauncherNotRunnable,
launchers.FennecLauncher.check_is_runnable)
self.assertRaises(LauncherNotRunnable, launchers.FennecLauncher.check_is_runnable)
# or if ADBHost().devices() raise an unexpected IOError
devices.side_effect = ADBError()
self.assertRaises(LauncherNotRunnable,
launchers.FennecLauncher.check_is_runnable)
self.assertRaises(LauncherNotRunnable, launchers.FennecLauncher.check_is_runnable)
@patch('time.sleep')
@patch('mozregression.launchers.FennecLauncher._create_profile')
@patch("time.sleep")
@patch("mozregression.launchers.FennecLauncher._create_profile")
def test_wait(self, _create_profile, sleep):
# Force use of existing profile
_create_profile.return_value = self.profile
@ -299,7 +294,7 @@ class TestFennecLauncher(unittest.TestCase):
self.adb.process_exist = Mock(side_effect=proc_exists)
launcher.start()
launcher.wait()
self.adb.process_exist.assert_called_with('org.mozilla.fennec')
self.adb.process_exist.assert_called_with("org.mozilla.fennec")
class Zipfile(object):
@ -313,47 +308,42 @@ class Zipfile(object):
pass
def extractall(self, dirname):
fname = 'js' if launchers.mozinfo.os != 'win' else 'js.exe'
with open(os.path.join(dirname, fname), 'w') as f:
f.write('1')
fname = "js" if launchers.mozinfo.os != "win" else "js.exe"
with open(os.path.join(dirname, fname), "w") as f:
f.write("1")
@pytest.mark.parametrize("mos,binary_name", [
('win', 'js.exe'),
('linux', 'js'),
('mac', 'js'),
])
@pytest.mark.parametrize("mos,binary_name", [("win", "js.exe"), ("linux", "js"), ("mac", "js")])
def test_jsshell_install(mocker, mos, binary_name):
zipfile = mocker.patch('mozregression.launchers.zipfile')
zipfile = mocker.patch("mozregression.launchers.zipfile")
zipfile.ZipFile = Zipfile
mocker.patch('mozregression.launchers.mozinfo').os = mos
mocker.patch("mozregression.launchers.mozinfo").os = mos
with launchers.JsShellLauncher('/path/to') as js:
with launchers.JsShellLauncher("/path/to") as js:
assert os.path.isdir(js.tempdir)
assert os.path.basename(js.binary) == binary_name
assert not os.path.isdir(js.tempdir)
def test_jsshell_install_except(mocker):
mocker.patch('mozregression.launchers.zipfile').ZipFile.side_effect \
= Exception
mocker.patch("mozregression.launchers.zipfile").ZipFile.side_effect = Exception
with pytest.raises(Exception):
launchers.JsShellLauncher('/path/to')
launchers.JsShellLauncher("/path/to")
@pytest.mark.parametrize("return_code", [0, 1])
def test_jsshell_start(mocker, return_code):
zipfile = mocker.patch('mozregression.launchers.zipfile')
zipfile = mocker.patch("mozregression.launchers.zipfile")
zipfile.ZipFile = Zipfile
call = mocker.patch('mozregression.launchers.call')
call = mocker.patch("mozregression.launchers.call")
call.return_code = return_code
logger = Mock()
with launchers.JsShellLauncher('/path/to') as js:
with launchers.JsShellLauncher("/path/to") as js:
js._logger = logger
js.start()
assert js.get_app_info() == {}

Просмотреть файл

@ -1,9 +1,12 @@
from __future__ import absolute_import
import re
import pytest
from mozregression import log
from six import StringIO
from colorama import Fore, Style
from six import StringIO
from mozregression import log
def init_logger(mocker, **kwargs):
@ -41,5 +44,6 @@ def test_logger_debug(mocker, debug):
def test_colorize():
assert log.colorize("stuff", allow_color=True) == "stuff"
assert log.colorize("{fRED}stuff{sRESET_ALL}", allow_color=True) == (
Fore.RED + "stuff" + Style.RESET_ALL)
Fore.RED + "stuff" + Style.RESET_ALL
)
assert log.colorize("{fRED}stuf{sRESET_ALL}", allow_color=False) == "stuf"

Просмотреть файл

@ -1,42 +1,45 @@
from __future__ import absolute_import
import pytest
from argparse import ArgumentParser, Namespace
import pytest
from mozregression import __version__
from mozregression.config import DEFAULTS
from mozregression.mach_interface import new_release_on_pypi, parser, run
@pytest.mark.parametrize('pypi_version, result', [
(lambda: 'latest', 'latest'),
(lambda: __version__, None), # same version, None is returned
(lambda: None, None),
(Exception, None), # on exception, None is returned
])
@pytest.mark.parametrize(
"pypi_version, result",
[
(lambda: "latest", "latest"),
(lambda: __version__, None), # same version, None is returned
(lambda: None, None),
(Exception, None), # on exception, None is returned
],
)
def test_new_release_on_pypi(mocker, pypi_version, result):
pypi_latest_version = mocker.patch(
'mozregression.mach_interface.pypi_latest_version'
)
pypi_latest_version = mocker.patch("mozregression.mach_interface.pypi_latest_version")
pypi_latest_version.side_effect = pypi_version
assert new_release_on_pypi() == result
def test_parser(mocker):
defaults = dict(DEFAULTS)
defaults.update({'persist': 'stuff'})
get_defaults = mocker.patch('mozregression.mach_interface.get_defaults')
defaults.update({"persist": "stuff"})
get_defaults = mocker.patch("mozregression.mach_interface.get_defaults")
get_defaults.return_value = defaults
p = parser()
assert isinstance(p, ArgumentParser)
options = p.parse_args(['--persist-size-limit=1'])
options = p.parse_args(["--persist-size-limit=1"])
assert options.persist == 'stuff'
assert options.persist == "stuff"
assert options.persist_size_limit == 1.0
def test_run(mocker):
main = mocker.patch('mozregression.mach_interface.main')
run({'persist': 'foo', 'bits': 64})
main.assert_called_once_with(check_new_version=False,
namespace=Namespace(bits=64, persist='foo'))
main = mocker.patch("mozregression.mach_interface.main")
run({"persist": "foo", "bits": 64})
main.assert_called_once_with(
check_new_version=False, namespace=Namespace(bits=64, persist="foo")
)

Просмотреть файл

@ -2,22 +2,21 @@
# 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
from __future__ import print_function
import pytest
from __future__ import absolute_import, print_function
import unittest
import requests
from datetime import date
from mock import patch, Mock, MagicMock, ANY
from mozregression import main, errors, __version__, config
from mozregression.test_runner import ManualTestRunner, CommandTestRunner
from mozregression.download_manager import BuildDownloadManager
from mozregression.bisector import Bisector, Bisection, IntegrationHandler, \
NightlyHandler
import pytest
import requests
from mock import ANY, MagicMock, Mock, patch
from six.moves import range
from mozregression import __version__, config, errors, main
from mozregression.bisector import Bisection, Bisector, IntegrationHandler, NightlyHandler
from mozregression.download_manager import BuildDownloadManager
from mozregression.test_runner import CommandTestRunner, ManualTestRunner
class AppCreator(object):
def __init__(self, logger):
@ -47,40 +46,44 @@ class AppCreator(object):
@pytest.yield_fixture
def create_app(mocker):
"""allow to create an Application and ensure that clear() is called"""
creator = AppCreator(mocker.patch('mozregression.main.LOG'))
creator = AppCreator(mocker.patch("mozregression.main.LOG"))
yield creator
creator.clear()
def test_app_get_manual_test_runner(create_app):
app = create_app(['--profile=/prof'])
app = create_app(["--profile=/prof"])
assert isinstance(app.test_runner, ManualTestRunner)
assert app.test_runner.launcher_kwargs == dict(
addons=[], profile='/prof', cmdargs=['--allow-downgrade'], preferences=[],
adb_profile_dir=None
addons=[],
profile="/prof",
cmdargs=["--allow-downgrade"],
preferences=[],
adb_profile_dir=None,
)
def test_app_get_command_test_runner(create_app):
app = create_app(['--command=echo {binary}'])
app = create_app(["--command=echo {binary}"])
assert isinstance(app.test_runner, CommandTestRunner)
assert app.test_runner.command == 'echo {binary}'
assert app.test_runner.command == "echo {binary}"
@pytest.mark.parametrize("argv,background_dl_policy,size_limit", [
([], "cancel", 0),
# without persist, cancel policy is forced
(['--background-dl-policy=keep'], "cancel", 0),
(['--persist=1', "--background-dl-policy=keep"], "keep", 0),
# persist limit
(['--persist-size-limit=10'], "cancel", 10 * 1073741824),
])
def test_app_get_download_manager(create_app, argv, background_dl_policy,
size_limit):
@pytest.mark.parametrize(
"argv,background_dl_policy,size_limit",
[
([], "cancel", 0),
# without persist, cancel policy is forced
(["--background-dl-policy=keep"], "cancel", 0),
(["--persist=1", "--background-dl-policy=keep"], "keep", 0),
# persist limit
(["--persist-size-limit=10"], "cancel", 10 * 1073741824),
],
)
def test_app_get_download_manager(create_app, argv, background_dl_policy, size_limit):
app = create_app(argv)
assert isinstance(app.build_download_manager, BuildDownloadManager)
assert app.build_download_manager.background_dl_policy == \
background_dl_policy
assert app.build_download_manager.background_dl_policy == background_dl_policy
assert app.build_download_manager.persist_limit.size_limit == size_limit
assert app.build_download_manager.persist_limit.file_limit == 5
@ -91,75 +94,62 @@ def test_app_get_bisector(create_app):
def test_app_bisect_nightlies_finished(create_app, mocker):
app = create_app(['-g=2015-06-01', '-b=2015-06-02'])
app = create_app(["-g=2015-06-01", "-b=2015-06-02"])
app.bisector.bisect = Mock(return_value=Bisection.FINISHED)
app._bisect_integration = Mock(return_value=0)
NightlyHandler = mocker.patch(
"mozregression.main.NightlyHandler"
)
nh = Mock(bad_date=date.today(), good_revision='c1',
bad_revision='c2')
NightlyHandler = mocker.patch("mozregression.main.NightlyHandler")
nh = Mock(bad_date=date.today(), good_revision="c1", bad_revision="c2")
NightlyHandler.return_value = nh
assert app.bisect_nightlies() == 0
app.bisector.bisect.assert_called_once_with(
ANY,
date(2015, 0o6, 0o1),
date(2015, 0o6, 0o2)
)
assert create_app.find_in_log(
"Got as far as we can go bisecting nightlies..."
)
app._bisect_integration.assert_called_once_with(
'c1', 'c2', expand=config.DEFAULT_EXPAND)
app.bisector.bisect.assert_called_once_with(ANY, date(2015, 0o6, 0o1), date(2015, 0o6, 0o2))
assert create_app.find_in_log("Got as far as we can go bisecting nightlies...")
app._bisect_integration.assert_called_once_with("c1", "c2", expand=config.DEFAULT_EXPAND)
def test_app_bisect_nightlies_no_data(create_app):
app = create_app(['-g=2015-06-01', '-b=2015-06-02'])
app = create_app(["-g=2015-06-01", "-b=2015-06-02"])
app.bisector.bisect = Mock(return_value=Bisection.NO_DATA)
assert app.bisect_nightlies() == 1
assert create_app.find_in_log(
"Unable to get valid builds within the given range.",
False
)
assert create_app.find_in_log("Unable to get valid builds within the given range.", False)
@pytest.mark.parametrize("same_chsets", [True, False])
def test_app_bisect_integration_finished(create_app, same_chsets):
argv = [
'--good=c1',
'--bad=%s' % ('c1' if same_chsets else 'c2')
]
argv = ["--good=c1", "--bad=%s" % ("c1" if same_chsets else "c2")]
app = create_app(argv)
app.bisector.bisect = Mock(return_value=Bisection.FINISHED)
assert app.bisect_integration() == 0
assert create_app.find_in_log("No more integration revisions, bisection finished.")
if same_chsets:
assert create_app.find_in_log("It seems that you used two changesets"
" that are in the same push.", False)
assert create_app.find_in_log(
"It seems that you used two changesets" " that are in the same push.", False
)
@pytest.mark.parametrize("argv,expected_log", [
(['--app=firefox', '--bits=64'], "--app=firefox --bits=64"),
(['--persist', 'blah stuff'], "--persist 'blah stuff'"),
(['--addon=a b c', '--addon=d'], "'--addon=a b c' --addon=d"),
(['--find-fix', '--arg=a b'], "--find-fix '--arg=a b'"),
(['--profile=pro file'], "'--profile=pro file'"),
(['-g', '2015-11-01'], "--good=2015-11-01"),
(['--bad=2015-11-03'], "--bad=2015-11-03"),
])
def test_app_bisect_nightlies_user_exit(create_app, argv, expected_log,
mocker):
@pytest.mark.parametrize(
"argv,expected_log",
[
(["--app=firefox", "--bits=64"], "--app=firefox --bits=64"),
(["--persist", "blah stuff"], "--persist 'blah stuff'"),
(["--addon=a b c", "--addon=d"], "'--addon=a b c' --addon=d"),
(["--find-fix", "--arg=a b"], "--find-fix '--arg=a b'"),
(["--profile=pro file"], "'--profile=pro file'"),
(["-g", "2015-11-01"], "--good=2015-11-01"),
(["--bad=2015-11-03"], "--bad=2015-11-03"),
],
)
def test_app_bisect_nightlies_user_exit(create_app, argv, expected_log, mocker):
Handler = mocker.patch("mozregression.main.NightlyHandler")
Handler.return_value = Mock(
build_range=[Mock(repo_name="mozilla-central")],
good_date='2015-11-01',
bad_date='2015-11-03',
spec=NightlyHandler
good_date="2015-11-01",
bad_date="2015-11-03",
spec=NightlyHandler,
)
app = create_app(argv)
app.bisector.bisect = Mock(return_value=Bisection.USER_EXIT)
sys = mocker.patch('mozregression.main.sys')
sys = mocker.patch("mozregression.main.sys")
sys.argv = argv
assert app.bisect_nightlies() == 0
assert create_app.find_in_log("To resume, run:")
@ -169,12 +159,14 @@ def test_app_bisect_nightlies_user_exit(create_app, argv, expected_log,
def test_app_bisect_integration_user_exit(create_app, mocker):
Handler = mocker.patch("mozregression.main.IntegrationHandler")
Handler.return_value = Mock(build_range=[Mock(repo_name="mozilla-central")],
good_revision='c1',
bad_revision='c2',
spec=IntegrationHandler)
Handler.return_value = Mock(
build_range=[Mock(repo_name="mozilla-central")],
good_revision="c1",
bad_revision="c2",
spec=IntegrationHandler,
)
app = create_app(['--good=c1', '--bad=c2'])
app = create_app(["--good=c1", "--bad=c2"])
app.bisector.bisect = Mock(return_value=Bisection.USER_EXIT)
assert app.bisect_integration() == 0
assert create_app.find_in_log("To resume, run:")
@ -182,20 +174,17 @@ def test_app_bisect_integration_user_exit(create_app, mocker):
def test_app_bisect_integration_no_data(create_app):
app = create_app(['--good=c1', '--bad=c2'])
app = create_app(["--good=c1", "--bad=c2"])
app.bisector.bisect = Mock(return_value=Bisection.NO_DATA)
assert app.bisect_integration() == 1
assert create_app.find_in_log(
"There are no build artifacts for these changesets",
False
)
assert create_app.find_in_log("There are no build artifacts for these changesets", False)
def test_app_bisect_ctrl_c_exit(create_app, mocker):
app = create_app([])
app.bisector.bisect = Mock(side_effect=KeyboardInterrupt)
at_exit = mocker.patch('atexit.register')
handler = MagicMock(good_revision='c1', bad_revision='c2')
at_exit = mocker.patch("atexit.register")
handler = MagicMock(good_revision="c1", bad_revision="c2")
Handler = mocker.patch("mozregression.main.NightlyHandler")
Handler.return_value = handler
with pytest.raises(KeyboardInterrupt):
@ -209,25 +198,25 @@ def test_app_bisect_ctrl_c_exit(create_app, mocker):
class TestCheckMozregresionVersion(unittest.TestCase):
@patch('mozregression.main.LOG')
@patch('requests.get')
@patch("mozregression.main.LOG")
@patch("requests.get")
def test_version_is_upto_date(self, get, log):
response = Mock(json=lambda: {'info': {'version': __version__}})
response = Mock(json=lambda: {"info": {"version": __version__}})
get.return_value = response
main.check_mozregression_version()
self.assertFalse(log.critical.called)
@patch('requests.get')
@patch("requests.get")
def test_Exception_error(self, get):
get.side_effect = requests.RequestException
# exception is handled inside main.check_mozregression_version
main.check_mozregression_version()
self.assertRaises(requests.RequestException, get)
@patch('mozregression.main.LOG')
@patch('requests.get')
@patch("mozregression.main.LOG")
@patch("requests.get")
def test_warn_if_version_is_not_up_to_date(self, get, log):
response = Mock(json=lambda: {'info': {'version': 0}})
response = Mock(json=lambda: {"info": {"version": 0}})
get.return_value = response
main.check_mozregression_version()
self.assertEqual(log.warning.call_count, 2)
@ -238,26 +227,28 @@ class TestMain(unittest.TestCase):
self.app = Mock()
self.logger = Mock()
@patch('mozregression.main.LOG')
@patch('mozregression.main.check_mozregression_version')
@patch('mozlog.structured.commandline.setup_logging')
@patch('mozregression.main.set_http_session')
@patch('mozregression.main.Application')
def do_cli(self, argv, Application, set_http_session,
setup_logging, check_mozregression_version, log):
@patch("mozregression.main.LOG")
@patch("mozregression.main.check_mozregression_version")
@patch("mozlog.structured.commandline.setup_logging")
@patch("mozregression.main.set_http_session")
@patch("mozregression.main.Application")
def do_cli(
self, argv, Application, set_http_session, setup_logging, check_mozregression_version, log,
):
self.logger = log
def create_app(fetch_config, options):
self.app.fetch_config = fetch_config
self.app.options = options
return self.app
Application.side_effect = create_app
try:
main.main(argv)
except SystemExit as exc:
return exc.code
else:
self.fail('mozregression.main.cli did not call sys.exit')
self.fail("mozregression.main.cli did not call sys.exit")
def pop_logs(self):
logs = []
@ -268,7 +259,7 @@ class TestMain(unittest.TestCase):
def pop_exit_error_msg(self):
for lvl, msg in reversed(self.pop_logs()):
if lvl == 'error':
if lvl == "error":
return msg
def test_without_args(self):
@ -281,7 +272,7 @@ class TestMain(unittest.TestCase):
def test_bisect_integration(self):
self.app.bisect_integration.return_value = 0
exitcode = self.do_cli(['--good=a1', '--bad=b5'])
exitcode = self.do_cli(["--good=a1", "--bad=b5"])
self.assertEqual(exitcode, 0)
self.app.bisect_integration.assert_called_with()
@ -289,15 +280,14 @@ class TestMain(unittest.TestCase):
# KeyboardInterrupt is handled with a nice error message.
self.app.bisect_nightlies.side_effect = KeyboardInterrupt
exitcode = self.do_cli([])
self.assertIn('Interrupted', exitcode)
self.assertIn("Interrupted", exitcode)
def test_handle_mozregression_errors(self):
# Any MozRegressionError subclass is handled with a nice error message
self.app.bisect_nightlies.side_effect = \
errors.MozRegressionError('my error')
self.app.bisect_nightlies.side_effect = errors.MozRegressionError("my error")
exitcode = self.do_cli([])
self.assertNotEqual(exitcode, 0)
self.assertIn('my error', self.pop_exit_error_msg())
self.assertIn("my error", self.pop_exit_error_msg())
def test_handle_other_errors(self):
# other exceptions are just thrown as usual

Просмотреть файл

@ -1,71 +1,75 @@
from __future__ import absolute_import
import unittest
from mock import patch, Mock
from mock import Mock, patch
from mozregression import network
class TestUrlLinks(unittest.TestCase):
@patch('requests.get')
@patch("requests.get")
def test_url_no_links(self, get):
get.return_value = Mock(text='')
self.assertEqual(network.url_links(''), [])
get.return_value = Mock(text="")
self.assertEqual(network.url_links(""), [])
@patch('requests.get')
@patch("requests.get")
def test_url_with_links(self, get):
get.return_value = Mock(text="""
get.return_value = Mock(
text="""
<body>
<a href="thing/">thing</a>
<a href="thing2/">thing2</a>
</body>
""")
self.assertEqual(network.url_links(''),
['thing/', 'thing2/'])
"""
)
self.assertEqual(network.url_links(""), ["thing/", "thing2/"])
@patch('requests.get')
@patch("requests.get")
def test_url_with_links_regex(self, get):
get.return_value = Mock(text="""
get.return_value = Mock(
text="""
<body>
<a href="thing/">thing</a>
<a href="thing2/">thing2</a>
</body>
""")
self.assertEqual(
network.url_links('', regex="thing2.*"),
['thing2/'])
"""
)
self.assertEqual(network.url_links("", regex="thing2.*"), ["thing2/"])
@patch('requests.get')
@patch("requests.get")
def test_url_with_absolute_links(self, get):
get.return_value = Mock(text="""
get.return_value = Mock(
text="""
<body>
<a href="/useless/thing/">thing</a>
<a href="/useless/thing2">thing2</a>
</body>
""")
self.assertEqual(network.url_links(''),
['thing/', 'thing2'])
"""
)
self.assertEqual(network.url_links(""), ["thing/", "thing2"])
def test_set_http_session():
try:
with patch('requests.Session') as Session:
with patch("requests.Session") as Session:
session = Session.return_value = Mock()
session_get = session.get
network.set_http_session(get_defaults={'timeout': 5})
network.set_http_session(get_defaults={"timeout": 5})
assert session == network.get_http_session()
# timeout = 5 will be passed to the original get method as a default
session.get('http://my-ul')
session_get.assert_called_with('http://my-ul', timeout=5)
session.get("http://my-ul")
session_get.assert_called_with("http://my-ul", timeout=5)
# if timeout is defined, it will override the default
session.get('http://my-ul', timeout=10)
session_get.assert_called_with('http://my-ul', timeout=10)
session.get("http://my-ul", timeout=10)
session_get.assert_called_with("http://my-ul", timeout=10)
finally:
# remove the global session to not impact other tests
network.SESSION = None
if __name__ == '__main__':
if __name__ == "__main__":
unittest.main()

Просмотреть файл

@ -1,10 +1,12 @@
from __future__ import absolute_import
import pytest
import os
import tempfile
import mozfile
import time
import mozfile
import pytest
from mozregression.persist_limit import PersistLimit
@ -17,8 +19,8 @@ class TempCreator(object):
def create_file(self, name, size, delay):
fname = os.path.join(self.tempdir, name)
with open(fname, 'w') as f:
f.write('a' * size)
with open(fname, "w") as f:
f.write("a" * size)
# equivalent to touch, but we apply a delay for the test
atime = time.time() + delay
os.utime(fname, (atime, atime))
@ -31,16 +33,19 @@ def temp():
mozfile.remove(tmp.tempdir)
@pytest.mark.parametrize("size_limit,file_limit,files", [
# limit_file is always respected
(10, 5, "bcdef"),
(10, 3, "def"),
# if size_limit or file_limit is 0, nothing is removed
(0, 5, "abcdef"),
(5, 0, "abcdef"),
# limit_size works
(35, 1, "def"),
])
@pytest.mark.parametrize(
"size_limit,file_limit,files",
[
# limit_file is always respected
(10, 5, "bcdef"),
(10, 3, "def"),
# if size_limit or file_limit is 0, nothing is removed
(0, 5, "abcdef"),
(5, 0, "abcdef"),
# limit_size works
(35, 1, "def"),
],
)
def test_persist_limit(temp, size_limit, file_limit, files):
temp.create_file("a", 10, -6)
temp.create_file("b", 10, -5)
@ -53,4 +58,4 @@ def test_persist_limit(temp, size_limit, file_limit, files):
persist_limit.register_dir_content(temp.tempdir)
persist_limit.remove_old_files()
assert ''.join(sorted(temp.list())) == ''.join(sorted(files))
assert "".join(sorted(temp.list())) == "".join(sorted(files))

Просмотреть файл

@ -1,10 +1,15 @@
from __future__ import absolute_import
import unittest
from mozregression import errors
from mozregression.releases import (releases, formatted_valid_release_dates,
date_of_release, tag_of_release,
tag_of_beta)
from mozregression.releases import (
date_of_release,
formatted_valid_release_dates,
releases,
tag_of_beta,
tag_of_release,
)
class TestRelease(unittest.TestCase):
@ -15,7 +20,7 @@ class TestRelease(unittest.TestCase):
self.assertEqual(date, "2012-06-05")
date = date_of_release(34)
self.assertEqual(date, "2014-09-02")
date = date_of_release('33')
date = date_of_release("33")
self.assertEqual(date, "2014-07-21")
def test_valid_formatted_release_dates(self):
@ -39,36 +44,36 @@ class TestRelease(unittest.TestCase):
with self.assertRaises(errors.UnavailableRelease):
date_of_release(441)
with self.assertRaises(errors.UnavailableRelease):
date_of_release('ew21rtw112')
date_of_release("ew21rtw112")
def test_valid_release_tags(self):
tag = tag_of_release('57.0')
tag = tag_of_release("57.0")
self.assertEqual(tag, "FIREFOX_57_0_RELEASE")
tag = tag_of_release('60')
tag = tag_of_release("60")
self.assertEqual(tag, "FIREFOX_60_0_RELEASE")
tag = tag_of_release('65.0.1')
tag = tag_of_release("65.0.1")
self.assertEqual(tag, "FIREFOX_65_0_1_RELEASE")
def test_invalid_release_tags(self):
with self.assertRaises(errors.UnavailableRelease):
tag_of_release('55.0.1.1')
tag_of_release("55.0.1.1")
with self.assertRaises(errors.UnavailableRelease):
tag_of_release('57.0b4')
tag_of_release("57.0b4")
with self.assertRaises(errors.UnavailableRelease):
tag_of_release('abc')
tag_of_release("abc")
def test_valid_beta_tags(self):
tag = tag_of_beta('57.0b9')
tag = tag_of_beta("57.0b9")
self.assertEqual(tag, "FIREFOX_57_0b9_RELEASE")
tag = tag_of_beta('60.0b12')
tag = tag_of_beta("60.0b12")
self.assertEqual(tag, "FIREFOX_60_0b12_RELEASE")
tag = tag_of_beta('65')
tag = tag_of_beta("65")
self.assertEqual(tag, "FIREFOX_RELEASE_65_BASE")
tag = tag_of_beta('66.0')
tag = tag_of_beta("66.0")
self.assertEqual(tag, "FIREFOX_RELEASE_66_BASE")
def test_invalid_beta_tags(self):
with self.assertRaises(errors.UnavailableRelease):
tag_of_beta('57.0.1')
tag_of_beta("57.0.1")
with self.assertRaises(errors.UnavailableRelease):
tag_of_beta('xyz')
tag_of_beta("xyz")

Просмотреть файл

@ -3,14 +3,16 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
from __future__ import absolute_import
import pytest
import unittest
import datetime
from mock import patch, Mock
from mozregression import test_runner, errors, build_info
import datetime
import unittest
import pytest
from mock import Mock, patch
from six.moves import range
from mozregression import build_info, errors, test_runner
def mockinfo(**kwargs):
return Mock(spec=build_info.BuildInfo, **kwargs)
@ -32,81 +34,69 @@ class TestManualTestRunner(unittest.TestCase):
def setUp(self):
self.runner = test_runner.ManualTestRunner()
@patch('mozregression.test_runner.mozlauncher')
@patch("mozregression.test_runner.mozlauncher")
def test_nightly_create_launcher(self, create_launcher):
launcher = Mock()
create_launcher.return_value = launcher
info = mockinfo(
build_type='nightly',
app_name="firefox",
build_file="/path/to"
)
info = mockinfo(build_type="nightly", app_name="firefox", build_file="/path/to")
result_launcher = test_runner.create_launcher(info)
create_launcher.\
assert_called_with(info)
create_launcher.assert_called_with(info)
self.assertEqual(result_launcher, launcher)
@patch('mozregression.test_runner.mozlauncher')
@patch('mozregression.test_runner.LOG')
@patch("mozregression.test_runner.mozlauncher")
@patch("mozregression.test_runner.LOG")
def test_nightly_create_launcher_buildid(self, log, mozlauncher):
launcher = Mock()
mozlauncher.return_value = launcher
info = mockinfo(
build_type='nightly',
build_type="nightly",
app_name="firefox",
build_file="/path/to",
build_date=datetime.datetime(2015, 11, 6, 5, 4, 3),
repo_name='mozilla-central',
repo_name="mozilla-central",
)
result_launcher = test_runner.create_launcher(info)
mozlauncher.\
assert_called_with(info)
log.info.assert_called_with(
'Running mozilla-central build for buildid 20151106050403')
mozlauncher.assert_called_with(info)
log.info.assert_called_with("Running mozilla-central build for buildid 20151106050403")
self.assertEqual(result_launcher, launcher)
@patch('mozregression.download_manager.DownloadManager.download')
@patch('mozregression.test_runner.mozlauncher')
@patch("mozregression.download_manager.DownloadManager.download")
@patch("mozregression.test_runner.mozlauncher")
def test_inbound_create_launcher(self, mozlauncher, download):
launcher = Mock()
mozlauncher.return_value = launcher
info = mockinfo(
build_type='inbound',
app_name="firefox",
build_file="/path/to"
)
info = mockinfo(build_type="inbound", app_name="firefox", build_file="/path/to")
result_launcher = test_runner.create_launcher(info)
mozlauncher.assert_called_with(info)
self.assertEqual(result_launcher, launcher)
@patch('mozregression.test_runner.input')
@patch("mozregression.test_runner.input")
def test_get_verdict(self, input):
input.return_value = 'g'
verdict = self.runner.get_verdict(mockinfo(build_type='inbound'),
False)
self.assertEqual(verdict, 'g')
input.return_value = "g"
verdict = self.runner.get_verdict(mockinfo(build_type="inbound"), False)
self.assertEqual(verdict, "g")
output = input.call_args[0][0]
# bad is proposed
self.assertIn('bad', output)
self.assertIn("bad", output)
# back is not
self.assertNotIn('back', output)
self.assertNotIn("back", output)
@patch('mozregression.test_runner.input')
@patch("mozregression.test_runner.input")
def test_get_verdict_allow_back(self, input):
input.return_value = 'back'
verdict = self.runner.get_verdict(mockinfo(build_type='inbound'), True)
input.return_value = "back"
verdict = self.runner.get_verdict(mockinfo(build_type="inbound"), True)
output = input.call_args[0][0]
# back is now proposed
self.assertIn('back', output)
self.assertEqual(verdict, 'back')
self.assertIn("back", output)
self.assertEqual(verdict, "back")
@patch('mozregression.test_runner.create_launcher')
@patch('mozregression.test_runner.ManualTestRunner.get_verdict')
@patch("mozregression.test_runner.create_launcher")
@patch("mozregression.test_runner.ManualTestRunner.get_verdict")
def test_evaluate(self, get_verdict, create_launcher):
get_verdict.return_value = 'g'
get_verdict.return_value = "g"
launcher = Mock()
create_launcher.return_value = Launcher(launcher)
build_infos = mockinfo()
@ -117,13 +107,12 @@ class TestManualTestRunner(unittest.TestCase):
launcher.start.assert_called_with()
get_verdict.assert_called_with(build_infos, False)
launcher.stop.assert_called_with()
self.assertEqual(result[0], 'g')
self.assertEqual(result[0], "g")
@patch('mozregression.test_runner.create_launcher')
@patch('mozregression.test_runner.ManualTestRunner.get_verdict')
def test_evaluate_with_launcher_error_on_stop(self, get_verdict,
create_launcher):
get_verdict.return_value = 'g'
@patch("mozregression.test_runner.create_launcher")
@patch("mozregression.test_runner.ManualTestRunner.get_verdict")
def test_evaluate_with_launcher_error_on_stop(self, get_verdict, create_launcher):
get_verdict.return_value = "g"
launcher = Mock(stop=Mock(side_effect=errors.LauncherError))
create_launcher.return_value = Launcher(launcher)
build_infos = mockinfo()
@ -131,9 +120,9 @@ class TestManualTestRunner(unittest.TestCase):
# the LauncherError is silently ignore here
launcher.stop.assert_called_with()
self.assertEqual(result[0], 'g')
self.assertEqual(result[0], "g")
@patch('mozregression.test_runner.create_launcher')
@patch("mozregression.test_runner.create_launcher")
def test_run_once(self, create_launcher):
launcher = Mock(wait=Mock(return_value=0))
create_launcher.return_value = Launcher(launcher)
@ -144,7 +133,7 @@ class TestManualTestRunner(unittest.TestCase):
launcher.start.assert_called_with()
launcher.wait.assert_called_with()
@patch('mozregression.test_runner.create_launcher')
@patch("mozregression.test_runner.create_launcher")
def test_run_once_ctrlc(self, create_launcher):
launcher = Mock(wait=Mock(side_effect=KeyboardInterrupt))
create_launcher.return_value = Launcher(launcher)
@ -159,115 +148,111 @@ class TestManualTestRunner(unittest.TestCase):
class TestCommandTestRunner(unittest.TestCase):
def setUp(self):
self.runner = test_runner.CommandTestRunner('my command')
self.runner = test_runner.CommandTestRunner("my command")
self.launcher = Mock()
del self.launcher.binary # block the auto attr binary on the mock
if not hasattr(self, 'assertRaisesRegex'):
if not hasattr(self, "assertRaisesRegex"):
self.assertRaisesRegex = self.assertRaisesRegexp
def test_create(self):
self.assertEqual(self.runner.command, 'my command')
self.assertEqual(self.runner.command, "my command")
@patch('mozregression.test_runner.create_launcher')
@patch('subprocess.call')
def evaluate(self, call, create_launcher, build_info={},
retcode=0, subprocess_call_effect=None):
build_info['app_name'] = 'myapp'
@patch("mozregression.test_runner.create_launcher")
@patch("subprocess.call")
def evaluate(
self, call, create_launcher, build_info={}, retcode=0, subprocess_call_effect=None,
):
build_info["app_name"] = "myapp"
call.return_value = retcode
if subprocess_call_effect:
call.side_effect = subprocess_call_effect
self.subprocess_call = call
create_launcher.return_value = Launcher(self.launcher)
return self.runner.evaluate(
mockinfo(to_dict=lambda: build_info)
)[0]
return self.runner.evaluate(mockinfo(to_dict=lambda: build_info))[0]
def test_evaluate_retcode(self):
self.assertEqual('g', self.evaluate(retcode=0))
self.assertEqual('b', self.evaluate(retcode=1))
self.assertEqual("g", self.evaluate(retcode=0))
self.assertEqual("b", self.evaluate(retcode=1))
def test_supbrocess_call(self):
self.evaluate()
command = self.subprocess_call.mock_calls[0][1][0]
kwargs = self.subprocess_call.mock_calls[0][2]
self.assertEqual(command, ['my', 'command'])
self.assertIn('env', kwargs)
self.assertEqual(command, ["my", "command"])
self.assertIn("env", kwargs)
def test_env_vars(self):
self.evaluate(build_info={'my': 'var', 'int': 15})
self.evaluate(build_info={"my": "var", "int": 15})
expected = {
'MOZREGRESSION_MY': 'var',
'MOZREGRESSION_INT': '15',
'MOZREGRESSION_APP_NAME': 'myapp',
"MOZREGRESSION_MY": "var",
"MOZREGRESSION_INT": "15",
"MOZREGRESSION_APP_NAME": "myapp",
}
passed_env = self.subprocess_call.mock_calls[0][2]['env']
passed_env = self.subprocess_call.mock_calls[0][2]["env"]
self.assertTrue(set(expected).issubset(set(passed_env)))
def test_command_placeholder_replaced(self):
self.runner.command = 'run {app_name} "1"'
self.evaluate()
command = self.subprocess_call.mock_calls[0][1][0]
self.assertEqual(command, ['run', 'myapp', '1'])
self.assertEqual(command, ["run", "myapp", "1"])
self.runner.command = 'run \'{binary}\' "{foo}"'
self.launcher.binary = 'mybinary'
self.evaluate(build_info={'foo': 12})
self.runner.command = "run '{binary}' \"{foo}\""
self.launcher.binary = "mybinary"
self.evaluate(build_info={"foo": 12})
command = self.subprocess_call.mock_calls[0][1][0]
self.assertEqual(command, ['run', 'mybinary', '12'])
self.assertEqual(command, ["run", "mybinary", "12"])
def test_command_placeholder_error(self):
self.runner.command = 'run {app_nam} "1"'
self.assertRaisesRegex(errors.TestCommandError,
'formatting',
self.evaluate)
self.assertRaisesRegex(errors.TestCommandError, "formatting", self.evaluate)
def test_command_empty_error(self):
# in case the command line is empty,
# subprocess.call will raise IndexError
self.assertRaisesRegex(errors.TestCommandError,
'Empty', self.evaluate,
subprocess_call_effect=IndexError)
self.assertRaisesRegex(
errors.TestCommandError, "Empty", self.evaluate, subprocess_call_effect=IndexError,
)
def test_command_missing_error(self):
# in case the command is missing or not executable,
# subprocess.call will raise IOError
self.assertRaisesRegex(errors.TestCommandError,
'not found', self.evaluate,
subprocess_call_effect=OSError)
self.assertRaisesRegex(
errors.TestCommandError, "not found", self.evaluate, subprocess_call_effect=OSError,
)
def test_run_once(self):
self.runner.evaluate = Mock(return_value='g')
self.runner.evaluate = Mock(return_value="g")
build_info = Mock()
self.assertEqual(self.runner.run_once(build_info), 0)
self.runner.evaluate.assert_called_once_with(build_info)
# useful fixture
from .test_build_range import range_creator # noqa
@pytest.mark.parametrize('brange,input,allowed_range,result', [ # noqa
# [0, 1, 2, 3, 4, 5] (6 elements, mid is '3')
(list(range(6)), ['-2'], '[-2, 1]', 1),
# [0, 1, 2, 3, 4] (5 elements, mid is '2')
(list(range(5)), ['1'], '[-1, 1]', 3),
# user hit something bad, we loop
(list(range(5)), ['aa', '', '1'], '[-1, 1]', 3),
# small range, no input
(list(range(3)), Exception('input called, it should not happen'), None, 1)
])
def test_index_to_try_after_skip(mocker, range_creator, brange,
input, allowed_range, result):
@pytest.mark.parametrize(
"brange,input,allowed_range,result",
[ # noqa
# [0, 1, 2, 3, 4, 5] (6 elements, mid is '3')
(list(range(6)), ["-2"], "[-2, 1]", 1),
# [0, 1, 2, 3, 4] (5 elements, mid is '2')
(list(range(5)), ["1"], "[-1, 1]", 3),
# user hit something bad, we loop
(list(range(5)), ["aa", "", "1"], "[-1, 1]", 3),
# small range, no input
(list(range(3)), Exception("input called, it should not happen"), None, 1),
],
)
def test_index_to_try_after_skip(mocker, range_creator, brange, input, allowed_range, result):
build_range = range_creator.create(brange)
mocked_input = mocker.patch("mozregression.test_runner.input")
mocked_input.side_effect = input
output = []
mocked_stdout = mocker.patch('sys.stdout')
mocked_stdout = mocker.patch("sys.stdout")
mocked_stdout.write = output.append
runner = test_runner.ManualTestRunner()
assert runner.index_to_try_after_skip(build_range) == result
if allowed_range is not None:
assert ("You can choose a build index between %s:"
% allowed_range) in [o.strip() for o in output]
assert ("You can choose a build index between %s:" % allowed_range) in [
o.strip() for o in output
]