diff --git a/fabfile/__init__.py b/fabfile/__init__.py index 67a68967..a3625162 100644 --- a/fabfile/__init__.py +++ b/fabfile/__init__.py @@ -17,7 +17,7 @@ For more help on a particular command from fabric.api import env -from fabfile.tasks import code_quality +from fabfile.tasks import code_quality, test # Without this, `fab -l` would display the whole docstring as preamble @@ -26,6 +26,7 @@ __doc__ = "" # This list defines which tasks are made available to the user __all__ = [ "code_quality", + "test", ] # Honour the user's ssh client configuration diff --git a/fabfile/lib.py b/fabfile/lib.py index e22af53a..7af40231 100644 --- a/fabfile/lib.py +++ b/fabfile/lib.py @@ -6,8 +6,10 @@ A library of functions and constants for tasks to make use of. import os import sys +import re +from functools import wraps -from fabric.api import run, hide +from fabric.api import run, hide, cd, env from fabric.context_managers import settings, shell_env from fabvenv import virtualenv @@ -18,6 +20,14 @@ VENV_ROOT = os.path.expanduser(os.path.join('~', '.virtualenvs', 'pybitmessage-d PYTHONPATH = os.path.join(PROJECT_ROOT, 'src',) +def coerce_list(value): + """Coerce a value into a list""" + if isinstance(value, str): + return value.split(',') + else: + sys.exit("Bad string value {}".format(value)) + + def coerce_bool(value): """Coerce a value into a boolean""" if isinstance(value, bool): @@ -51,6 +61,29 @@ def flatten(data): return result +def filelist_from_git(rev=None): + """Return a list of files based on git output""" + cmd = 'git diff --name-only' + if rev: + if rev in ['cached', 'staged']: + cmd += ' --{}'.format(rev) + elif rev == 'working': + pass + else: + cmd += ' -r {}'.format(rev) + + with cd(PROJECT_ROOT): + with hide('running', 'stdout'): + results = [] + ansi_escape = re.compile(r'\x1B\[[0-?]*[ -/]*[@-~]') + clean = ansi_escape.sub('', run(cmd)) + clean = re.sub('\n', '', clean) + for line in clean.split('\r'): + if line.endswith(".py"): + results.append(os.path.abspath(line)) + return results + + def pycodestyle(path_to_file): """Run pycodestyle on a file""" with virtualenv(VENV_ROOT): @@ -148,3 +181,20 @@ def get_filtered_pylint_output(path_to_file): not i.startswith('Using config file'), ]) ] + + +def default_hosts(hosts): + """Decorator to apply default hosts to a task""" + + def real_decorator(func): + """Manipulate env""" + env.hosts = env.hosts or hosts + + @wraps(func) + def wrapper(*args, **kwargs): + """Original function called from here""" + return func(*args, **kwargs) + + return wrapper + + return real_decorator diff --git a/fabfile/tasks.py b/fabfile/tasks.py index 98ab2f0e..83d9784b 100644 --- a/fabfile/tasks.py +++ b/fabfile/tasks.py @@ -1,8 +1,6 @@ # pylint: disable=not-context-manager """ Fabric tasks for PyBitmessage devops operations. - -# pylint: disable=not-context-manager """ import os @@ -12,7 +10,7 @@ from fabric.api import run, task, hide, cd from fabvenv import virtualenv from fabfile.lib import ( - autopep8, PROJECT_ROOT, VENV_ROOT, coerce_bool, flatten, + autopep8, PROJECT_ROOT, VENV_ROOT, coerce_bool, flatten, filelist_from_git, default_hosts, get_filtered_pycodestyle_output, get_filtered_flake8_output, get_filtered_pylint_output, ) @@ -28,9 +26,9 @@ def get_tool_results(file_list): result['pylint_violations'] = get_filtered_pylint_output(path_to_file) result['path_to_file'] = path_to_file result['total_violations'] = sum([ - len(result['pycodestyle_violations']), - len(result['flake8_violations']), - len(result['pylint_violations']), + len(result['pycodestyle_violations']), + len(result['flake8_violations']), + len(result['pylint_violations']), ]) results.append(result) return results @@ -39,7 +37,7 @@ def get_tool_results(file_list): def print_results(results, top, verbose, details): """Print an item with the appropriate verbosity / detail""" - if verbose: + if verbose and results: print ''.join( [ os.linesep, @@ -117,7 +115,8 @@ def generate_file_list(filename): @task -def code_quality(verbose=True, details=False, fix=False, filename=None, top=10): +@default_hosts(['localhost']) +def code_quality(verbose=True, details=False, fix=False, filename=None, top=10, rev=None): """ Check code quality. @@ -128,6 +127,8 @@ def code_quality(verbose=True, details=False, fix=False, filename=None, top=10): $ fab -H localhost code_quality + :param rev: If not None, act on files changed since this commit. 'cached/staged' and 'working' have special meaning + :type rev: str or None, default None :param top: Display / fix only the top N violating files, a value of 0 will display / fix all files :type top: int, default 10 :param verbose: Display a header and the counts, without this you just get the filenames in order @@ -146,13 +147,14 @@ def code_quality(verbose=True, details=False, fix=False, filename=None, top=10): Intended to be temporary until we have improved code quality and have safeguards to maintain it in place. """ + # pylint: disable=too-many-arguments verbose = coerce_bool(verbose) details = coerce_bool(details) fix = coerce_bool(fix) top = int(top) or -1 - file_list = generate_file_list(filename) + file_list = generate_file_list(filename) if not rev else filelist_from_git(rev) results = get_tool_results(file_list) if fix: @@ -163,3 +165,18 @@ def code_quality(verbose=True, details=False, fix=False, filename=None, top=10): print_results(results, top, verbose, details) sys.exit(sum([item['total_violations'] for item in results])) + + +@task +@default_hosts(['localhost']) +def test(): + """Run tests on the code""" + + with cd(PROJECT_ROOT): + with virtualenv(VENV_ROOT): + + run('pip uninstall -y pybitmessage') + run('python setup.py install') + + run('pybitmessage -t') + run('python setup.py test')