Merge pull request #197 from parkouss/download-background

Download next builds in background
This commit is contained in:
Julien Pagès 2015-03-12 21:41:48 +01:00
Родитель 1bf8970df5 0bb81e571f
Коммит 315bb7c72f
12 изменённых файлов: 866 добавлений и 270 удалений

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

@ -6,9 +6,12 @@
import math
import datetime
import tempfile
import mozfile
from mozlog.structured import get_default_logger
from mozregression.build_data import NightlyBuildData, InboundBuildData
from mozregression.download_manager import BuildDownloadManager
def compute_steps_left(steps):
@ -43,11 +46,14 @@ class BisectorHandler(object):
"""
self.build_data = build_data
def build_infos(self, index):
def build_infos(self, index, fetch_config):
"""
Compute build infos (a dict) when a build is about to be tested.
"""
infos = {'build_type': self.build_type}
infos = {
'build_type': self.build_type,
'app_name': fetch_config.app_name
}
infos.update(self.build_data[index])
return infos
@ -141,9 +147,10 @@ class NightlyHandler(BisectorHandler):
self._reverse_if_find_fix(self.build_data.get_associated_data(0),
self.build_data.get_associated_data(-1))
def build_infos(self, index):
infos = BisectorHandler.build_infos(self, index)
def build_infos(self, index, fetch_config):
infos = BisectorHandler.build_infos(self, index, fetch_config)
infos['build_date'] = self.build_data.get_associated_data(index)
infos['repo'] = fetch_config.get_nightly_repo(infos['build_date'])
return infos
def _print_progress(self, new_data):
@ -206,6 +213,11 @@ class InboundHandler(BisectorHandler):
build_data_class = InboundBuildData
build_type = 'inbound'
def build_infos(self, index, fetch_config):
infos = BisectorHandler.build_infos(self, index, fetch_config)
infos['repo'] = fetch_config.inbound_branch
return infos
def _print_progress(self, new_data):
self._logger.info("Narrowed inbound regression window from [%s, %s]"
" (%d revisions) to [%s, %s] (%d revisions)"
@ -235,9 +247,22 @@ class Bisector(object):
FINISHED = 2
USER_EXIT = 3
def __init__(self, fetch_config, test_runner):
def __init__(self, fetch_config, test_runner, persist=None,
dl_in_background=True):
self.fetch_config = fetch_config
self.test_runner = test_runner
self.delete_dldir = False
if persist is None:
# always keep the downloaded files
# this allows to not re-download a file if a user retry a build.
persist = tempfile.mkdtemp()
self.delete_dldir = True
self.download_dir = persist
self.dl_in_background = dl_in_background
def __del__(self):
if self.delete_dldir:
mozfile.remove(self.download_dir)
def bisect(self, handler, good, bad, **kwargs):
if handler.find_fix:
@ -246,9 +271,17 @@ class Bisector(object):
good,
bad,
**kwargs)
return self._bisect(handler, build_data)
download_manager = \
BuildDownloadManager(handler._logger,
self.download_dir)
try:
return self._bisect(download_manager, handler, build_data)
finally:
# ensure we cancel any possible download done in the background
# else python will block until threads termination.
download_manager.cancel()
def _bisect(self, handler, build_data):
def _bisect(self, download_manager, handler, build_data):
"""
Starts a bisection for a :class:`mozregression.build_data.BuildData`.
"""
@ -267,7 +300,36 @@ class Bisector(object):
handler.finished()
return self.FINISHED
build_infos = handler.build_infos(mid)
build_infos = handler.build_infos(mid, self.fetch_config)
dest = download_manager.focus_download(build_infos)
# start downloading the next builds.
# note that we don't have to worry if builds are already
# downloaded, or if our build infos are the same because
# this will be handled by the downloadmanager.
if self.dl_in_background:
def start_dl(r):
# first get the next mid point
# this will trigger some blocking downloads
# (we need to find the build info)
m = r.mid_point()
if len(r) != 0:
# this is a trick to call build_infos
# with the the appropriate build_data
handler.set_build_data(r)
try:
# non-blocking download of the build
download_manager.download_in_background(
handler.build_infos(m, self.fetch_config))
finally:
# put the real build_data back
handler.set_build_data(build_data)
# download next left mid point
start_dl(build_data[mid:])
# download right next mid point
start_dl(build_data[:mid+1])
build_infos['build_path'] = dest
verdict, app_info = \
self.test_runner.evaluate(build_infos,
allow_back=bool(previous_data))
@ -323,7 +385,9 @@ class BisectRunner(object):
def __init__(self, fetch_config, test_runner, options):
self.fetch_config = fetch_config
self.options = options
self.bisector = Bisector(fetch_config, test_runner)
self.bisector = Bisector(fetch_config, test_runner,
persist=options.persist,
dl_in_background=options.background_dl)
self._logger = get_default_logger('Bisector')
def do_bisect(self, handler, good, bad, **kwargs):

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

@ -0,0 +1,323 @@
import threading
import requests
from contextlib import closing
import os
import sys
class DownloadInterrupt(Exception):
pass
class Download(object):
"""
Download is reponsible of downloading one file in the background.
Example of use: ::
dl = Download(url, dest)
dl.start()
dl.wait() # this will block until completion / cancel / error
If a download fail or is canceled, the temporary dest is removed from
the disk.
:param url: the url of the file to download
:param dest: the local file path destination
:param finished_callback: a callback that will be called in the thread
when the thread work is done. Takes the download
instance as a parameter.
:param chunk_size: size of the chunk that will be read. The thread can
not be stopped while we are reading that chunk size.
:param session: a requests.Session or the requests module that will do
do the real downloading work.
: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):
self.thread = threading.Thread(
target=self._download,
args=(url, dest, finished_callback, chunk_size, session)
)
self._lock = threading.Lock()
self.__url = url
self.__dest = dest
self.__progress = progress
self.__canceled = False
self.__error = None
def start(self):
"""
Start the thread that will do the download.
"""
self.thread.start()
def cancel(self):
"""
Cancel a previously started download.
"""
self.__canceled = True
def is_canceled(self):
"""
Returns True if we canceled this download.
"""
return self.__canceled
def is_running(self):
"""
Returns True if the downloading thread is running.
"""
return self.thread.is_alive()
def wait(self, raise_if_error=True):
"""
Block until the downloading thread is finished.
:param raise_if_error: if True (the default), :meth:`raise_if_error`
will be called and raise an error if any.
"""
while self.thread.is_alive():
try:
# in case of exception here (like KeyboardInterrupt),
# cancel the task.
self.thread.join(0.02)
except:
self.cancel()
raise
# this will raise exception that may happen inside the thread.
if raise_if_error:
self.raise_if_error()
def error(self):
"""
Returns None or a tuple of three values (type, value, traceback)
that give information about the exception.
"""
return self.__error
def raise_if_error(self):
"""
Raise an error if any. If the download was canceled, raise
:class:`DownloadInterrupt`.
"""
if self.__error:
raise self.__error[0], self.__error[1], self.__error[2]
if self.__canceled:
raise DownloadInterrupt()
def set_progress(self, progress):
"""
set a callable to report the progress of the download, or None to
disable any report.
The callable must take three parameters (download, current, total).
Note that this method is thread safe, you can call it during a
download.
"""
with self._lock:
self.__progress = progress
def get_dest(self):
"""
Returns the dest.
"""
return self.__dest
def get_url(self):
"""
Returns the url.
"""
return self.__url
def _update_progress(self, current, total):
with self._lock:
if self.__progress:
self.__progress(self, current, total)
def _download(self, url, dest, finished_callback, chunk_size, session):
bytes_so_far = 0
try:
with closing(session.get(url, stream=True)) as response:
total_size = int(response.headers['Content-length'].strip())
self._update_progress(bytes_so_far, total_size)
with open(dest, 'wb') as f:
for chunk in response.iter_content(chunk_size):
if self.is_canceled():
break
if chunk:
f.write(chunk)
bytes_so_far += len(chunk)
self._update_progress(bytes_so_far, total_size)
except:
self.__error = sys.exc_info()
try:
if (self.is_canceled() or self.__error) and os.path.exists(dest):
os.unlink(dest)
finally:
if finished_callback:
finished_callback(self)
class DownloadManager(object):
"""
DownloadManager is responsible of starting and managing downloads inside
a given directory. It will download a file only if a given filename
is not already there.
Downloadmanager itself is not thread safe, and must not be shared
between threads.
Note that backgound downloads needs to be stopped. For example, if
you have an exception while a download is occuring, python will only
exit when the download will finish. To get rid of that, there is a
possible idiom: ::
def download_things(manager):
# do things with the manager
manager.download(url1, f1)
manager.download(url2, f2)
...
manager = DownloadManager(destdir)
try:
download_things(manager)
finally:
# ensure we cancel all background downloads to ask the end
# of possible remainings threads
manager.cancel()
"""
def __init__(self, destdir, session=requests):
self.destdir = destdir
self.session = session
self._downloads = {}
self._lock = threading.Lock()
def get_dest(self, fname):
return os.path.join(self.destdir, fname)
def cancel(self, cancel_if=None):
"""
Cancel downloads, if any.
if cancel_if is given, it must be a callable that take the download
instance as parameter, and return True if the download needs to be
canceled.
Note that download threads won't be stopped directly.
"""
with self._lock:
for download in self._downloads.itervalues():
if cancel_if is None or cancel_if(download):
if download.is_running():
download.cancel()
def download(self, url, fname):
"""
Returns a started download instance, or None if fname is already
present in destdir.
if a download is already running for the given fname, it is just
returned. Else the download is created, started and returned.
"""
dest = self.get_dest(fname)
with self._lock:
# if we are downloading, just returns the instance
if dest in self._downloads:
return self._downloads[dest]
if os.path.exists(dest):
return None
# else create the download (will be automatically removed of
# the list on completion) start it, and returns that.
def remove_download(_):
with self._lock:
del self._downloads[dest]
with self._lock:
download = Download(url, dest,
session=self.session,
finished_callback=remove_download)
self._downloads[dest] = download
download.start()
return download
def download_progress(_dl, bytes_so_far, total_size):
percent = (float(bytes_so_far) / total_size) * 100
sys.stdout.write("===== Downloaded %d%% =====\r" % percent)
sys.stdout.flush()
class BuildDownloadManager(DownloadManager):
"""
A DownloadManager specialized to download builds.
"""
def __init__(self, logger, destdir, session=requests):
DownloadManager.__init__(self, destdir, session=session)
self.logger = logger
self._downloads_bg = set()
def _extract_download_info(self, build_info):
if build_info['build_type'] == 'nightly':
persist_prefix = '%(build_date)s--%(repo)s--' % build_info
else:
persist_prefix = '%(timestamp)s--%(repo)s--' % build_info
build_url = build_info['build_url']
fname = persist_prefix + os.path.basename(build_url)
return build_url, fname
def download_in_background(self, build_info):
"""
Start a build download in background.
Don nothing is a build is already downloading/downloaded.
"""
build_url, fname = self._extract_download_info(build_info)
result = self.download(build_url, fname)
if result is not None:
self._downloads_bg.add(fname)
return result
def focus_download(self, build_info):
"""
Start a download for a build and focus on it.
*focus* here means that if there are running downloads for other
builds they will be canceled. Also, the progress is attached so
the user can see the download progress.
If the download of the build is already running, it will just
attach the progress function. If the build has already been
downloaded, it will do nothing.
this methods block until the build is available, or any error
occurs.
Returns the complete path of the downloaded build.
"""
build_url, fname = self._extract_download_info(build_info)
dest = self.get_dest(fname)
# first, stop all downloads in background (except the one for this
# build if any)
self.cancel(cancel_if=lambda dl: dest != dl.get_dest())
dl = self.download(build_url, fname)
if dl:
self.logger.info("Downloading build from: %s" % build_url)
dl.set_progress(download_progress)
try:
dl.wait()
finally:
print '' # a new line after download_progress calls
else:
msg = "Using local file: %s" % dest
if fname in self._downloads_bg:
msg += " (downloaded in background)"
self.logger.info(msg)
return dest

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

@ -16,9 +16,8 @@ from mozdevice import ADBAndroid, ADBHost
import mozversion
import mozinstall
import tempfile
import os
from mozregression.utils import ClassRegistry, download_url
from mozregression.utils import ClassRegistry
from mozregression.errors import LauncherNotRunnable
@ -37,26 +36,11 @@ class Launcher(object):
"""
pass
def __init__(self, url, persist=None, persist_prefix=''):
def __init__(self, dest):
self._running = False
self._logger = get_default_logger('Test Runner')
basename = os.path.basename(url)
if persist:
dest = os.path.join(persist, '%s%s' % (persist_prefix, basename))
if not os.path.exists(dest):
self._download(url, dest)
else:
self._logger.info("Using local file: %s" % dest)
else:
dest = basename
self._download(url, dest)
try:
self._install(dest)
finally:
if not persist:
os.unlink(dest)
self._install(dest)
def start(self, **kwargs):
"""
@ -83,10 +67,6 @@ class Launcher(object):
def __del__(self):
self.stop()
def _download(self, url, dest):
self._logger.info("Downloading build from: %s" % url)
download_url(url, dest)
def _install(self, dest):
raise NotImplementedError
@ -146,13 +126,11 @@ class MozRunnerLauncher(Launcher):
REGISTRY = ClassRegistry('app_name')
def create_launcher(name, url, persist=None, persist_prefix=''):
def create_launcher(name, dest):
"""
Create and returns an instance launcher for the given name.
"""
return REGISTRY.get(name)(url,
persist=persist,
persist_prefix=persist_prefix)
return REGISTRY.get(name)(dest)
@REGISTRY.register('firefox')

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

@ -183,6 +183,13 @@ def parse_args(argv=None):
" %(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.get('no-background-dl', '').lower()
not in ('1', 'yes', 'true')),
help=("Do not download next builds in the background"
" while evaluating the current build."))
commandline.add_logging_group(parser)
options = parser.parse_args(argv)
options.bits = parse_bits(options.bits)
@ -334,12 +341,9 @@ def cli(argv=None):
cmdargs=options.cmdargs,
preferences=preference(options.prefs_files, options.prefs),
)
test_runner = ManualTestRunner(fetch_config,
persist=options.persist,
launcher_kwargs=launcher_kwargs)
test_runner = ManualTestRunner(launcher_kwargs=launcher_kwargs)
else:
test_runner = CommandTestRunner(fetch_config, options.command,
persist=options.persist)
test_runner = CommandTestRunner(options.command)
runner = ResumeInfoBisectRunner(fetch_config, test_runner, options)

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

@ -13,8 +13,6 @@ from mozlog.structured import get_default_logger
import subprocess
import shlex
import os
import tempfile
import mozfile
from mozregression.launchers import create_launcher
from mozregression.errors import TestCommandError
@ -26,10 +24,7 @@ class TestRunner(object):
:meth:`evaluate` must be implemented by subclasses.
"""
def __init__(self, fetch_config, persist=None, launcher_kwargs=None):
self.fetch_config = fetch_config
self.persist = persist
self.launcher_kwargs = launcher_kwargs or {}
def __init__(self):
self.logger = get_default_logger('Test Runner')
def create_launcher(self, build_info):
@ -37,22 +32,16 @@ class TestRunner(object):
Create and returns a :class:`mozregression.launchers.Launcher`.
"""
if build_info['build_type'] == 'nightly':
date = build_info['build_date']
nightly_repo = self.fetch_config.get_nightly_repo(date)
persist_prefix = '%s--%s--' % (date, nightly_repo)
self.logger.info("Running nightly for %s" % date)
self.logger.info("Running nightly for %s"
% build_info["build_date"])
else:
persist_prefix = '%s--%s--' % (build_info['timestamp'],
self.fetch_config.inbound_branch)
self.logger.info("Testing inbound build with timestamp %s,"
" revision %s"
% (build_info['timestamp'],
build_info['revision']))
build_url = build_info['build_url']
return create_launcher(self.fetch_config.app_name,
build_url,
persist=self.persist,
persist_prefix=persist_prefix)
return create_launcher(build_info['app_name'],
build_info['build_path'])
def evaluate(self, build_info, allow_back=False):
"""
@ -67,6 +56,7 @@ class TestRunner(object):
:meth:`mozregression.launchers.Launcher.get_app_info` for this
particular build.
:param build_path: the path to the build file to test
:param build_info: is a dict containing information about the build
to test. It is ensured to have the following keys:
- build_type ('nightly' or 'inbound')
@ -88,19 +78,9 @@ class ManualTestRunner(TestRunner):
A TestRunner subclass that run builds and ask for evaluation by
prompting in the terminal.
"""
def __init__(self, fetch_config, persist=None, launcher_kwargs=None):
self.delete_persist = False
if persist is None:
# always keep the downloaded files for manual runner
# this allows to not re-download a file if a user retry a build.
persist = tempfile.mkdtemp()
self.delete_persist = True
TestRunner.__init__(self, fetch_config, persist=persist,
launcher_kwargs=launcher_kwargs)
def __del__(self):
if self.delete_persist:
mozfile.remove(self.persist)
def __init__(self, launcher_kwargs=None):
TestRunner.__init__(self)
self.launcher_kwargs = launcher_kwargs or {}
def get_verdict(self, build_info, allow_back):
"""
@ -156,15 +136,14 @@ class CommandTestRunner(TestRunner):
with curly brackets. Example:
`mozmill -app firefox -b {binary} -t path/to/test.js`
"""
def __init__(self, fetch_config, command, **kwargs):
TestRunner.__init__(self, fetch_config, **kwargs)
def __init__(self, command):
TestRunner.__init__(self)
self.command = command
def evaluate(self, build_info, allow_back=False):
launcher = self.create_launcher(build_info)
app_info = launcher.get_app_info()
variables = dict((k, str(v)) for k, v in build_info.iteritems())
variables['app_name'] = launcher.app_name
if hasattr(launcher, 'binary'):
variables['binary'] = launcher.binary

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

@ -7,13 +7,10 @@ Utility functions and classes for mozregression.
"""
import datetime
import os
import re
import sys
from BeautifulSoup import BeautifulSoup
import mozinfo
import requests
import mozfile
import redo
from mozregression import errors
@ -124,48 +121,6 @@ def parse_bits(option_bits):
return mozinfo.bits
def update_download_progress(percent):
"""
Print realtime status of downloaded file.
"""
sys.stdout.write("===== Downloaded %d%% =====\r" % percent)
sys.stdout.flush()
if percent >= 100:
sys.stdout.write("\n")
def download_url(url, dest):
"""
Download a file given an url.
"""
chunk_size = 16 * 1024
bytes_so_far = 0.0
tmp_file = dest + ".part"
response = get_http_session().get(url, stream=True)
total_size = int(response.headers['Content-length'].strip())
try:
with open(tmp_file, 'wb') as ftmp:
# write the file to the tmp_file
for chunk in response.iter_content(chunk_size=chunk_size):
# Filter out Keep-Alive chunks.
if not chunk:
continue
bytes_so_far += chunk_size
ftmp.write(chunk)
percent = (bytes_so_far / total_size) * 100
update_download_progress(percent)
except:
if os.path.isfile(tmp_file):
mozfile.remove(tmp_file)
raise
# move the temp file to the dest
os.rename(tmp_file, dest)
return dest
def url_links(url, regex=None, auth=None):
"""
Returns a list of links that can be found on a given web page.

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

@ -5,13 +5,14 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import unittest
from mock import patch, Mock, call, MagicMock
from mock import patch, Mock, call, MagicMock, ANY
import datetime
from mozregression.bisector import (BisectorHandler, NightlyHandler,
InboundHandler, Bisector,
BisectRunner)
from mozregression.main import parse_args
from mozregression.fetch_configs import create_config
from mozregression import build_data
@ -73,14 +74,19 @@ class TestNightlyHandler(unittest.TestCase):
self.handler = NightlyHandler()
def test_build_infos(self):
fetch_config = create_config('fennec-2.3', 'linux', 64)
fetch_config.set_nightly_repo('my-repo')
def get_associated_data(index):
return index
new_data = MagicMock(get_associated_data=get_associated_data)
self.handler.set_build_data(new_data)
result = self.handler.build_infos(1)
result = self.handler.build_infos(1, fetch_config)
self.assertEqual(result, {
'build_type': 'nightly',
'build_date': 1,
'app_name': 'fennec',
'repo': 'my-repo'
})
@patch('mozregression.bisector.BisectorHandler.initialize')
@ -163,12 +169,17 @@ class TestInboundHandler(unittest.TestCase):
self.handler = InboundHandler()
def test_build_infos(self):
fetch_config = create_config('firefox', 'linux', 64)
fetch_config.set_inbound_branch('my-branch')
self.handler.set_build_data([{'changeset': '1', 'repository': 'my'}])
result = self.handler.build_infos(0)
result = self.handler.build_infos(0, fetch_config)
self.assertEqual(result, {
'changeset': '1',
'repository': 'my',
'build_type': 'inbound'
'build_type': 'inbound',
'app_name': 'firefox',
'repo': 'my-branch',
})
def test_print_progress(self):
@ -228,13 +239,17 @@ class MyBuildData(build_data.BuildData):
class TestBisector(unittest.TestCase):
def setUp(self):
self.handler = Mock(find_fix=False)
self.handler = MagicMock(find_fix=False)
self.test_runner = Mock()
self.bisector = Bisector(Mock(), self.test_runner)
self.bisector = Bisector(Mock(), self.test_runner,
dl_in_background=False)
self.bisector.download_background = False
self.dl_manager = Mock()
def test__bisect_no_data(self):
build_data = MyBuildData()
result = self.bisector._bisect(self.handler, build_data)
result = self.bisector._bisect(self.dl_manager, self.handler,
build_data)
# test that handler methods where called
self.handler.set_build_data.assert_called_with(build_data)
self.handler.no_data.assert_called_once_with()
@ -243,7 +258,8 @@ class TestBisector(unittest.TestCase):
def test__bisect_finished(self):
build_data = MyBuildData([1])
result = self.bisector._bisect(self.handler, build_data)
result = self.bisector._bisect(self.dl_manager, self.handler,
build_data)
# test that handler methods where called
self.handler.set_build_data.assert_called_with(build_data)
self.handler.finished.assert_called_once_with()
@ -259,7 +275,8 @@ class TestBisector(unittest.TestCase):
'application_repository': 'unused'
}
self.test_runner.evaluate = Mock(side_effect=evaluate)
result = self.bisector._bisect(self.handler, build_data)
result = self.bisector._bisect(self.dl_manager, self.handler,
build_data)
return {
'result': result,
}
@ -352,6 +369,31 @@ class TestBisector(unittest.TestCase):
# user exit
self.assertEqual(test_result['result'], Bisector.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'])
# check that set_build_data was called
self.handler.set_build_data.assert_has_calls([
call(MyBuildData([1, 2, 3, 4, 5])), # first call
call(MyBuildData([3, 4, 5])), # download backgound
call(MyBuildData([1, 2, 3, 4, 5])), # put back the right data
call(MyBuildData([1, 2, 3])), # download backgound
call(MyBuildData([1, 2, 3, 4, 5])), # put back the right data
call(MyBuildData([3, 4, 5])), # we answered good
call(MyBuildData([4, 5])), # download backgound
call(MyBuildData([3, 4, 5])), # put back the right data
call(MyBuildData([3, 4])), # download backgound
call(MyBuildData([3, 4, 5])), # put back the right data
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_data.ensure_limits_called)
# bisection is finished
self.assertEqual(test_result['result'], Bisector.FINISHED)
@patch('mozregression.bisector.Bisector._bisect')
def test_bisect(self, _bisect):
_bisect.return_value = 1
@ -362,7 +404,7 @@ class TestBisector(unittest.TestCase):
build_data_class.assert_called_with(self.bisector.fetch_config,
'g', 'b', s=1)
self.assertFalse(build_data.reverse.called)
_bisect.assert_called_with(self.handler, build_data)
_bisect.assert_called_with(ANY, self.handler, build_data)
self.assertEqual(result, 1)
@patch('mozregression.bisector.Bisector._bisect')
@ -374,7 +416,7 @@ class TestBisector(unittest.TestCase):
self.bisector.bisect(self.handler, 'g', 'b', s=1)
build_data_class.assert_called_with(self.bisector.fetch_config,
'b', 'g', s=1)
_bisect.assert_called_with(self.handler, build_data)
_bisect.assert_called_with(ANY, self.handler, build_data)
class TestBisectRunner(unittest.TestCase):

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

@ -0,0 +1,353 @@
#!/usr/bin/env python
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import unittest
import tempfile
import shutil
import os
import time
from mock import Mock, patch, ANY
from datetime import date
from mozregression import download_manager
def mock_session():
response = Mock()
session = Mock(get=Mock(return_value=response))
return session, response
def mock_response(response, data, wait=0):
def iter_content(chunk_size=4):
rest = data
while rest:
time.sleep(wait)
chunk = rest[:chunk_size]
rest = rest[chunk_size:]
yield chunk
response.headers = {'Content-length': str(len(data))}
response.iter_content = iter_content
class TestDownload(unittest.TestCase):
def setUp(self):
self.tempdir = tempfile.mkdtemp()
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)
def test_creation(self):
self.assertFalse(self.dl.is_canceled())
self.assertFalse(self.dl.is_running())
self.assertIsNone(self.dl.error())
self.assertEquals(self.dl.get_url(), 'http://url')
self.assertEquals(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('1234' * 4)
# no file present yet
self.assertFalse(os.path.exists(self.tempfile))
self.dl.start()
self.assertTrue(self.dl.is_running())
self.dl.wait()
self.assertFalse(self.dl.is_running())
self.finished.assert_called_with(self.dl)
# file has been downloaded
with open(self.tempfile) as f:
self.assertEquals(f.read(), '1234' * 4)
def test_download_cancel(self):
self.create_response('1234' * 1000, wait=0.01)
start = time.time()
self.dl.start()
time.sleep(0.1)
self.dl.cancel()
with self.assertRaises(download_manager.DownloadInterrupt):
self.dl.wait()
self.assertTrue(self.dl.is_canceled())
# response generation should have taken 1000 * 0.01 = 10 seconds.
# since we canceled, this must be lower.
self.assertTrue((time.time() - start) < 1.0)
# file was deleted
self.assertFalse(os.path.exists(self.tempfile))
# finished callback was called
self.finished.assert_called_with(self.dl)
def test_download_with_progress(self):
data = []
def update_progress(_dl, current, total):
data.append((_dl, current, total))
self.create_response('1234' * 4)
self.dl.set_progress(update_progress)
self.dl.start()
self.dl.wait()
self.assertEquals(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.assertEquals(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.iter_content.side_effect = IOError
self.dl.start()
with self.assertRaises(IOError):
self.dl.wait()
self.assertEquals(self.dl.error()[0], IOError)
# finished callback was called
self.finished.assert_called_with(self.dl)
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('1234' * 1000, wait=0.01)
original_join = self.dl.thread.join
it = iter('123')
def join(timeout=None):
next(it) # will throw StopIteration after a few calls
original_join(timeout)
self.dl.thread.join = join
start = time.time()
self.dl.start()
with self.assertRaises(StopIteration):
self.dl.wait()
self.assertTrue(self.dl.is_canceled())
# wait for the thread to finish
original_join()
# response generation should have taken 1000 * 0.01 = 10 seconds.
# since we got an error, this must be lower.
self.assertTrue((time.time() - start) < 1.0)
# file was deleted
self.assertFalse(os.path.exists(self.tempfile))
# finished callback was called
self.finished.assert_called_with(self.dl)
class TestDownloadManager(unittest.TestCase):
def setUp(self):
self.tempdir = tempfile.mkdtemp()
self.addCleanup(shutil.rmtree, self.tempdir)
self.dl_manager = download_manager.DownloadManager(self.tempdir)
def do_download(self, url, fname, data, wait=0):
session, response = mock_session()
mock_response(response, data, wait)
# patch the session, so the download will use that
self.dl_manager.session = session
return self.dl_manager.download(url, fname)
def test_download(self):
dl1 = self.do_download('http://foo', 'foo', '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', 'hello2' * 4, wait=0.02)
self.assertEquals(dl1, dl2)
# starting a download with another fname will trigger a new download
dl3 = self.do_download('http://bar', 'foo2', 'hello you' * 4)
self.assertIsInstance(dl3, download_manager.Download)
self.assertNotEquals(dl3, dl1)
# let's wait for the downloads to finish
dl3.wait()
dl1.wait()
# now if we try to download a fname that exists, None is returned
dl4 = self.do_download('http://bar', 'foo', '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.assertEquals(content('foo'), 'hello' * 4)
self.assertEquals(content('foo2'), 'hello you' * 4)
# download instances are removed from the manager (internal test)
self.assertEquals(self.dl_manager._downloads, {})
def test_cancel(self):
dl1 = self.do_download('http://foo', 'foo', 'foo' * 500, wait=0.02)
dl2 = self.do_download('http://foo', 'bar', 'bar' * 500, wait=0.02)
dl3 = self.do_download('http://foo', 'foobar', 'foobar' * 4)
# let's cancel only one
def cancel_if(dl):
if os.path.basename(dl.get_dest()) == 'foo':
return True
self.dl_manager.cancel(cancel_if=cancel_if)
self.assertTrue(dl1.is_canceled())
self.assertFalse(dl2.is_canceled())
self.assertFalse(dl3.is_canceled())
# wait for dl3
dl3.wait()
# cancel everything
self.dl_manager.cancel()
self.assertTrue(dl1.is_canceled())
self.assertTrue(dl2.is_canceled())
# dl3 is not canceled since it finished before
self.assertFalse(dl3.is_canceled())
# wait for the completion of dl1 and dl2 threads
dl1.wait(raise_if_error=False)
dl2.wait(raise_if_error=False)
# at the end, only dl3 has been downloaded
self.assertEquals(os.listdir(self.tempdir), ["foobar"])
with open(os.path.join(self.tempdir, 'foobar')) as f:
self.assertEquals(f.read(), 'foobar' * 4)
# download instances are removed from the manager (internal test)
self.assertEquals(self.dl_manager._downloads, {})
class TestDownloadProgress(unittest.TestCase):
@patch("sys.stdout")
def test_basic(self, stdout):
download_manager.download_progress(None, 50, 100)
stdout.write.assert_called_with("===== Downloaded 50% =====\r")
stdout.flush.assert_called_with()
class TestBuildDownloadManager(unittest.TestCase):
def setUp(self):
self.session, self.session_response = mock_session()
self.dl_manager = \
download_manager.BuildDownloadManager(Mock(), 'dest',
session=self.session)
def test__extract_download_info(self):
url, fname = self.dl_manager._extract_download_info({
'build_url': 'http://some/thing',
'build_type': 'nightly',
'build_date': date(2015, 01, 03),
'repo': 'my-repo',
})
self.assertEquals(url, 'http://some/thing')
self.assertEquals(fname, '2015-01-03--my-repo--thing')
url, fname = self.dl_manager._extract_download_info({
'build_url': 'http://some/thing',
'build_type': 'inbound',
'timestamp': '123456',
'repo': 'my-repo',
})
self.assertEquals(url, 'http://some/thing')
self.assertEquals(fname, '123456--my-repo--thing')
@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')
download.return_value = ANY
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)
self.assertEquals(result, ANY)
@patch("mozregression.download_manager.BuildDownloadManager."
"_extract_download_info")
def test_focus_download(self, extract):
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)
# fake some download activity
self.dl_manager._downloads = {
current_dest: curent_download,
other_dest: other_download,
}
curent_download.is_running = Mock(return_value=True)
other_download.is_running = Mock(return_value=True)
result = self.dl_manager.focus_download({'build': 'info'})
curent_download.set_progress.assert_called_with(
download_manager.download_progress)
self.assertFalse(curent_download.is_canceled())
curent_download.wait.assert_called_with()
self.assertTrue(other_download.is_canceled())
self.dl_manager.logger.info.assert_called_with(
"Downloading build from: http://foo/bar")
self.assertEquals(result, current_dest)
@patch("mozregression.download_manager.BuildDownloadManager."
"_extract_download_info")
@patch("mozregression.download_manager.BuildDownloadManager.download")
def test_focus_download_file_already_exists(self, download, extract):
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')
result = self.dl_manager.focus_download({'build': 'info'})
dest_file = os.path.join('dest', 'myfile')
self.dl_manager.logger.info.assert_called_with(
"Using local file: %s (downloaded in background)" % dest_file)
self.assertEquals(result, dest_file)

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

@ -1,7 +1,5 @@
from mozregression import launchers
import unittest
import tempfile
import mozfile
import os
from mock import patch, Mock
from mozprofile import Profile
@ -24,61 +22,8 @@ class MyLauncher(launchers.Launcher):
class TestLauncher(unittest.TestCase):
def setUp(self):
self.tempdir = tempfile.mkdtemp()
# move on tempdir since launchers without persists
# download in current dir
curdir = os.getcwd()
os.chdir(self.tempdir)
with open('123-persist.zip', 'w') as f:
f.write('test-content')
self.addCleanup(mozfile.rmtree, self.tempdir)
self.addCleanup(os.chdir, curdir)
def _fake_download(self, url, dest):
with open(dest, 'w') as f:
f.write('test-content')
@patch('mozregression.launchers.download_url')
def test_download_on_create(self, download_url):
download_url.side_effect = self._fake_download
launcher = MyLauncher('http://fake/file.tar.bz2')
# download_url was called
self.assertEqual(download_url.call_args, (('http://fake/file.tar.bz2',
'file.tar.bz2'),))
# it is installed
self.assertEqual(launcher.installed, 'file.tar.bz2')
# download file was removed
self.assertFalse(os.path.exists('file.tar.bz2'))
@patch('mozregression.launchers.download_url')
def test_persist_download_on_create(self, download_url):
download_url.side_effect = self._fake_download
launcher = MyLauncher('http://foo/persist.zip', persist=self.tempdir)
expected_dest = os.path.join(self.tempdir, 'persist.zip')
# file has been downloaded
self.assertEqual(download_url.call_args, (('http://foo/persist.zip',
expected_dest),))
# it is installed
self.assertEqual(launcher.installed, expected_dest)
# download file was not removed
self.assertTrue(os.path.exists(expected_dest))
def test_reuse_persist_file_on_create(self):
launcher = MyLauncher('http://foo/persist.zip',
persist=self.tempdir,
persist_prefix='123-')
expected_dest = os.path.join(self.tempdir, '123-persist.zip')
# file is installed
self.assertEqual(launcher.installed, expected_dest)
# but not removed
self.assertTrue(os.path.exists('123-persist.zip'))
def test_start_stop(self):
launcher = MyLauncher('http://foo/persist.zip',
persist=self.tempdir,
persist_prefix='123-')
launcher = MyLauncher('/foo/persist.zip')
self.assertFalse(launcher.started)
launcher.start()
# now it has been started
@ -95,11 +40,9 @@ class TestLauncher(unittest.TestCase):
class TestMozRunnerLauncher(unittest.TestCase):
@patch('mozregression.launchers.mozinstall')
@patch('mozregression.launchers.download_url')
@patch('mozregression.launchers.os.unlink')
def setUp(self, unlink, download_url, mozinstall):
def setUp(self, mozinstall):
mozinstall.get_binary.return_value = '/binary'
self.launcher = launchers.MozRunnerLauncher('http://binary')
self.launcher = launchers.MozRunnerLauncher('/binary')
# patch profile_class else we will have some temporary dirs not deleted
@patch('mozregression.launchers.MozRunnerLauncher.\
@ -162,20 +105,18 @@ profile_class', spec=Profile)
class TestFennecLauncher(unittest.TestCase):
@patch('mozregression.launchers.download_url')
@patch('mozregression.launchers.os.unlink')
@patch('mozregression.launchers.mozversion.get_version')
@patch('mozregression.launchers.ADBAndroid')
def create_launcher(self, ADBAndroid, get_version, *a, **kwargs):
def create_launcher(self, ADBAndroid, get_version, **kwargs):
self.adb = Mock()
ADBAndroid.return_value = self.adb
get_version.return_value = kwargs.get('version_value', {})
return launchers.FennecLauncher('http://binary')
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')
def test_start_stop(self):
launcher = self.create_launcher()

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

@ -190,12 +190,23 @@ class TestMainCli(unittest.TestCase):
assert_called_with(utils.parse_date(good[1]),
utils.parse_date(bad[1]))
def test_download_in_background_is_on_by_default(self):
self.do_cli([])
self.assertTrue(self.runner.options.background_dl)
def test_deactive_download_in_background(self):
self.do_cli(['--no-background-dl'])
self.assertFalse(self.runner.options.background_dl)
class TestResumeInfoBisectRunner(unittest.TestCase):
def setUp(self):
self.opts = Mock(persist=None)
@patch('mozregression.main.BisectRunner')
def test_do_bisect(self, BisectRunner):
BisectRunner.do_bisect.return_value = 0
runner = main.ResumeInfoBisectRunner(None, None, None)
runner = main.ResumeInfoBisectRunner(None, None, self.opts)
result = runner.do_bisect('handler', 'g', 'b', range=4)
self.assertEquals(result, 0)
@ -206,7 +217,7 @@ class TestResumeInfoBisectRunner(unittest.TestCase):
@patch('mozregression.main.BisectRunner')
def test_do_bisect_error(self, BisectRunner, register):
BisectRunner.do_bisect.side_effect = KeyboardInterrupt
runner = main.ResumeInfoBisectRunner(None, None, None)
runner = main.ResumeInfoBisectRunner(None, None, self.opts)
handler = Mock(good_revision=1, bad_revision=2)
with self.assertRaises(KeyboardInterrupt):
runner.do_bisect(handler, 'g', 'b')
@ -217,7 +228,7 @@ class TestResumeInfoBisectRunner(unittest.TestCase):
@patch('mozregression.main.BisectRunner')
def test_on_exit_print_resume_info(self, BisectRunner):
handler = Mock()
runner = main.ResumeInfoBisectRunner(None, None, None)
runner = main.ResumeInfoBisectRunner(None, None, self.opts)
runner.print_resume_info = Mock()
runner.on_exit_print_resume_info(handler)

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

@ -7,19 +7,13 @@
import unittest
from mock import patch, Mock
import datetime
import os
from mozregression.fetch_configs import create_config
from mozregression import test_runner, errors
class TestManualTestRunner(unittest.TestCase):
def setUp(self):
fetch_config = create_config('firefox', 'linux', 64)
fetch_config.set_nightly_repo('my-repo')
fetch_config.set_inbound_branch('my-branch')
self.runner = test_runner.ManualTestRunner(fetch_config,
persist='/path/to')
self.runner = test_runner.ManualTestRunner()
@patch('mozregression.test_runner.create_launcher')
def test_nightly_create_launcher(self, create_launcher):
@ -28,28 +22,33 @@ class TestManualTestRunner(unittest.TestCase):
result_launcher = self.runner.create_launcher({
'build_type': 'nightly',
'build_date': datetime.date(2014, 12, 25),
'build_url': 'http://my-url'
'build_url': 'http://my-url',
'repo': 'my-repo',
'app_name': 'firefox',
'build_path': '/path/to',
})
create_launcher.\
assert_called_with('firefox', 'http://my-url',
persist_prefix='2014-12-25--my-repo--',
persist='/path/to')
assert_called_with('firefox',
'/path/to')
self.assertEqual(result_launcher, launcher)
@patch('mozregression.download_manager.DownloadManager.download')
@patch('mozregression.test_runner.create_launcher')
def test_inbound_create_launcher(self, create_launcher):
def test_inbound_create_launcher(self, create_launcher, download):
launcher = Mock()
create_launcher.return_value = launcher
result_launcher = self.runner.create_launcher({
'build_type': 'inbound',
'timestamp': '123',
'revision': '12345678',
'build_url': 'http://my-url'
'build_url': 'http://my-url',
'repo': 'my-branch',
'app_name': 'firefox',
'build_path': '/path/to',
})
create_launcher.assert_called_with('firefox', 'http://my-url',
persist_prefix='123--my-branch--',
persist='/path/to')
create_launcher.assert_called_with('firefox',
'/path/to')
self.assertEqual(result_launcher, launcher)
@patch('__builtin__.raw_input')
@ -89,22 +88,11 @@ class TestManualTestRunner(unittest.TestCase):
launcher.stop.assert_called_with()
self.assertEqual(result[0], 'g')
def test_persist_none_is_overidden(self):
runner = test_runner.ManualTestRunner(self.runner.fetch_config,
persist=None)
persist = runner.persist
self.assertIsNotNone(persist)
self.assertTrue(os.path.isdir(persist))
# deleting the runner also delete the temp dir
del runner
self.assertFalse(os.path.exists(persist))
class TestCommandTestRunner(unittest.TestCase):
def setUp(self):
fetch_config = create_config('firefox', 'linux', 64)
self.runner = test_runner.CommandTestRunner(fetch_config, 'my command')
self.launcher = Mock(app_name='myapp')
self.runner = test_runner.CommandTestRunner('my command')
self.launcher = Mock()
del self.launcher.binary # block the auto attr binary on the mock
def test_create(self):
@ -114,6 +102,7 @@ class TestCommandTestRunner(unittest.TestCase):
@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

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

@ -1,9 +1,6 @@
import unittest
from mock import patch, Mock
import datetime
import tempfile
import shutil
import os
import requests
from mozregression import utils, errors, limitedfilecache
@ -62,46 +59,6 @@ class TestParseBits(unittest.TestCase):
self.assertEqual(utils.parse_bits('64'), 64)
class TestDownloadUrl(unittest.TestCase):
def setUp(self):
self.tempdir = tempfile.mkdtemp()
self.addCleanup(shutil.rmtree, self.tempdir)
@patch('requests.get')
def test_download(self, get):
self.data = """
hello,
this is a response.
""" * (1024 * 16)
def iter_content(chunk_size=1):
rest = self.data
while rest:
chunk = rest[:chunk_size]
rest = rest[chunk_size:]
yield chunk
response = Mock(headers={'Content-length': str(len(self.data))},
iter_content=iter_content)
get.return_value = response
fname = os.path.join(self.tempdir, 'some.content')
utils.download_url('http://toto', fname)
self.assertEquals(self.data, open(fname).read())
@patch('requests.get')
@patch('mozfile.remove')
def test_download_with_exception_remove_tempfile(self, remove, get):
response = Mock(headers={'Content-length': '10'},
iter_content=Mock(side_effect=Exception))
get.return_value = response
fname = os.path.join(self.tempdir, 'some.content')
self.assertRaises(Exception, utils.download_url, 'http://toto', fname)
remove.assert_called_once_with(fname + '.part')
class TestGetBuildUrl(unittest.TestCase):
def test_for_linux(self):
self.assertEqual(utils.get_build_regex('test', 'linux', 32),