Merge pull request #197 from parkouss/download-background
Download next builds in background
This commit is contained in:
Коммит
315bb7c72f
|
@ -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),
|
||||
|
|
Загрузка…
Ссылка в новой задаче