Skip to content

Commit

Permalink
Merge branch 'release/1.6.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
vmalloc committed May 6, 2018
2 parents 7b872c5 + a5d0ed1 commit aa3af1b
Show file tree
Hide file tree
Showing 104 changed files with 1,161 additions and 682 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,5 @@ TAGS
TAGS
.cache
.pytest_cache
AUTHORS
ChangeLog
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,4 @@ deploy:
on:
tags: true
repo: getslash/slash
python: "3.6"
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ test: env
.env/bin/py.test --cov=slash --cov-report=html tests

pylint: env
.env/bin/pylint -j 4 --rcfile=.pylintrc slash tests setup.py
.env/bin/pylint -j 4 --rcfile=.pylintrc slash tests setup.py doc

env: .env/.up-to-date

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Slash
| | |
|-----------------------|------------------------------------------------------------------------------------|
| Build Status | ![Build Status](https://secure.travis-ci.org/getslash/slash.png?branch=master,dev) |
| Supported Versions | ![Supported Versions](https://img.shields.io/badge/python-2.7%2C3.3%2C3.4%2C3.5-green.svg) |
| Supported Versions | ![Supported Versions](https://img.shields.io/badge/python-2.7%2C3.3%2C3.4%2C3.5%2C3.6-green.svg) |
| Latest Version | ![Latest Version](https://img.shields.io/pypi/v/slash.svg) |
| Test Coverage | ![Coverage Status](https://img.shields.io/coveralls/getslash/slash/develop.svg) |

Expand Down
2 changes: 2 additions & 0 deletions doc/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ Plugins

.. autofunction:: slash.plugins.active

.. autofunction:: slash.plugins.parallel_mode

.. autofunction:: slash.plugins.registers_on

.. autofunction:: slash.plugins.register_if
Expand Down
11 changes: 11 additions & 0 deletions doc/changelog.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
Changelog
=========

* :release:`1.6.0 <6-5-2018>`
* :feature:`771` Keyword arguments to ``registers_on`` now get forwarded to Gossip's ``register`` API
* :feature:`769` Added a new configuration flag, ``log.show_raw_param_values``, defaulting to ``False``. If set to True, log lines for beginnings of tests will contain actual parametrization values instead of format-safe strings.
* :feature:`528` ``slash.exclude`` can now exclude combinations of parameter values
* :bug:`783` Session errors in children are now handled and reported when running with parallel
* :feature:`785` Plugins can now be marked to indicate whether or not they support parallel
execution, using ``slash.plugins.parallel_mode``. To avoid errors, Slash assumes that unmarked
plugins do not support parallel execution.
* :feature:`779` Added ``config.root.run.project_name``, which can be configured to hold the name of the current project. It defaults to the name of the directory in which your project's .slashrc is located
* :bug:`772 major` Fix handling exceptions which raised from None in interactive session
* :feature:`782` Added new hooks: ``before_session_cleanup``, ``after_session_end``
* :release:`1.5.1 <10-3-2018>`
* :bug:`767` Fixed traceback variable capture for cases where ``self=None``
* :release:`1.5.0 <7-3-2018>`
Expand Down
3 changes: 2 additions & 1 deletion doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
# All configuration values have a default; values that are commented out
# serve to show the default.

import sys, os, ast
import os
import sys
import pkg_resources

nitpicky = True
Expand Down
2 changes: 1 addition & 1 deletion doc/config_doc.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def run(self):
section = nodes.section(names=["conf." + path])
self.state.document.note_explicit_target(section)
section.append(nodes.title(text=path))
section.append(nodes.strong(text="Default: {0}".format(leaf.get_value())))
section.append(nodes.strong(text="Default: {}".format(leaf.get_value())))
if leaf.metadata and "doc" in leaf.metadata:
section.append(nodes.paragraph(text=str(leaf.metadata["doc"])))
returned.append(section)
Expand Down
11 changes: 5 additions & 6 deletions doc/hook_list_doc.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from docutils import nodes
from docutils.parsers.rst import directives, Directive
from slash import hooks
from slash import hooks # pylint: disable=unused-import
import gossip


Expand All @@ -10,18 +10,17 @@ class HookListDoc(Directive):
optional_arguments = 0
def run(self):
returned = []
for hook in sorted(gossip.get_group("slash").get_hooks(), key=lambda hook:hook.name):
for hook in sorted(gossip.get_group("slash").get_hooks(), key=lambda hook: hook.name):
section = nodes.section(ids=[hook.name], names=['hooks.{}'.format(hook.name)])
self.state.document.note_explicit_target(section)
returned.append(section)
title = "slash.hooks.{0}".format(hook.name)
title = "slash.hooks.{}".format(hook.name)
args = hook.get_argument_names()
if args:
title += "({0})".format(", ".join(args))
title += "({})".format(", ".join(args))
section.append(nodes.title(text=title))
section.append(nodes.paragraph(text=hook.doc))
return returned

def setup(app):
def setup(app): # pylint: disable=unused-argument
directives.register_directive('hook_list_doc', HookListDoc)

17 changes: 17 additions & 0 deletions doc/parameters.rst
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,20 @@ This also works for parameters of fixtures (for more information about fixtures
...
Exclusions also work on sets of parameters:

.. code-block:: python
import slash
SUPPORTED_SIZES = [10, 15, 20, 25]
@slash.exclude(('car.size', 'car.color'), [(10, 'red'), (20, 'blue')])
def test_car(car):
...
@slash.parametrize('size', SUPPORTED_SIZES)
@slash.parametrize('color', ['red', 'green', 'blue'])
@slash.fixture
def car(size): # <-- red cars of size 10 and blue cars of size 20 will be skipped
...
37 changes: 37 additions & 0 deletions doc/plugins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -242,3 +242,40 @@ Additionally, it provides access to instances of registered plugins by their nam
This could be used to access plugin attributes whose modification (e.g. by fixtures) can alter the plugin's behavior.

.. LocalWords: plugins Plugin plugin inheritence
Plugins and Parallel Runs
-------------------------

.. index::
double: parallel; plugins

Not all plugins can support :ref:`parallel execution <parallel>`, and for others implementing
support for it can be much harder than supporting non-parallel runs alone.

To deal with this, in addition to possible mistakes or corruption caused by plugins incorrectly used
in parallel mode, Slash requires each plugin to indicate whether or not it supports parallel
execution. The assumption is that by default plugins do not support parallel runs at all.

To indicate that your plugin supports parallel execution, use the :func:`plugins.parallel_mode
<slash.plugins.parallel_mode>` marker:

.. code-block:: python
from slash.plugins import PluginInterface, parallel_mode
@parallel_mode('enabled')
class MyPlugin(PluginInterface):
...
``parallel_mode`` supports the following modes:

* ``disabled`` - meaning the plugin does not support parallel execution at all. This is the default.
* ``parent-only`` - meaning the plugin supports parallel execution, but should be active only on the
parent process.
* ``child-only`` - meaning the plugin should only be activated on worker/child processes executing
the actual tests.
* ``enabled`` - meaning the plugin supports parallel execution, both on parent and child.



7 changes: 3 additions & 4 deletions scripts/build_test_dir.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,9 @@ def main(self):
t = s.add_test()
if self._args.use_fixtures and index % _FIXTURE_FREQ == 0:
if index % 2 == 0:
f = t.depend_on_fixture(t.file.add_fixture())
t.depend_on_fixture(t.file.add_fixture())
else:
f = t.depend_on_fixture(s.slashconf.add_fixture())
#f.parametrize()
t.depend_on_fixture(s.slashconf.add_fixture())

if self._args.use_parameters and index % _PARAM_FREQ == 0:
t.parametrize()
Expand All @@ -51,7 +50,7 @@ def main(self):
elif element == 'i':
t.when_run.interrupt()
else:
parser.error("Unknown marker: {0!r}".format(element))
parser.error("Unknown marker: {!r}".format(element))

s.commit()

Expand Down
1 change: 1 addition & 0 deletions slash/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ def __enter__(self):
self.session = Session(reporter=self.get_reporter(), console_stream=self._report_stream)

trigger_hook.configure() # pylint: disable=no-member
plugins.manager.configure_for_parallel_mode()
plugins.manager.activate_pending_plugins()
cli_utils.configure_plugins_from_args(self._parsed_args)

Expand Down
10 changes: 5 additions & 5 deletions slash/assertions.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
_logger = logbook.Logger(__name__)

def _deprecated(func, message=None):
return deprecated(since='0.19.0', what='slash.should.{0.__name__}'.format(func),
return deprecated(since='0.19.0', what='slash.should.{.__name__}'.format(func),
message=message or 'Use plain assertions instead')(func)


Expand All @@ -26,7 +26,7 @@ def _assertion(a, b, msg=None):
op.inverse_func).to_expression(a, b))
raise TestFailed(msg)
_assertion.__name__ = name
_assertion.__doc__ = "Asserts **{0}**".format(
_assertion.__doc__ = "Asserts **{}**".format(
op.to_expression("ARG1", "ARG2"))
_assertion = _deprecated(_assertion)
return _assertion
Expand All @@ -41,15 +41,15 @@ def _assertion(a, msg=None):
op.inverse_func).to_expression(a))
raise TestFailed(msg)
_assertion.__name__ = name
_assertion.__doc__ = "Asserts **{0}**".format(op.to_expression("ARG"))
_assertion.__doc__ = "Asserts **{}**".format(op.to_expression("ARG"))
_assertion = _deprecated(_assertion)
return _assertion


def _get_message(msg, description):
if msg is None:
return description
return "{0} ({1})".format(msg, description)
return "{} ({})".format(msg, description)

equal = _binary_assertion("equal", operator.eq)
assert_equal = assert_equals = equal = equal
Expand Down Expand Up @@ -145,7 +145,7 @@ def __exit__(self, *exc_info):
expected_classes = self._expected_classes
if not isinstance(expected_classes, tuple):
expected_classes = (expected_classes, )
msg = "{0} not raised".format("/".join(e.__name__ for e in expected_classes))
msg = "{} not raised".format("/".join(e.__name__ for e in expected_classes))
if self._ensure_caught:
raise ExpectedExceptionNotCaught(msg, self._expected_classes)
_logger.debug(msg)
Expand Down
3 changes: 3 additions & 0 deletions slash/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@
"last_test_symlink": None // Doc("If set, specifies a symlink path to the last test log file in each run"),
"last_failed_symlink": None // Doc("If set, specifies a symlink path to the last failed test log file"),
"show_manual_errors_tb": True // Doc("Show tracebacks for errors added via slash.add_error"),
"show_raw_param_values": False // Doc("Makes test start logs contain the raw values of test parameters"),

"silence_loggers": [] // Doc("Logger names to silence"),
"format": None // Doc("Format of the log line, as passed on to logbook. None will use the default format"),
"console_format": None // Doc("Optional format to be used for console output. Defaults to the regular format"),
Expand All @@ -86,6 +88,7 @@
"repeat_each": 1 // Doc("Repeat each test a specified amount of times") // Cmdline(arg='--repeat-each', metavar="NUM_TIMES"),
"repeat_all": 1 // Doc("Repeat all suite a specified amount of times") // Cmdline(arg='--repeat-all', metavar="NUM_TIMES"),
"session_state_path": "~/.slash/last_session" // Doc("Where to keep last session serialized data"),
"project_name": None,
"project_customization_file_path": "./.slashrc",
"user_customization_file_path": "~/.slash/slashrc",
"resume_state_path": "~/.slash/session_states" // Doc("Path to store or load session's resume data"),
Expand Down
8 changes: 6 additions & 2 deletions slash/core/cleanup_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ def add_cleanup(self, _func, *args, **kwargs):
raise IncorrectScope('Incorrect scope specified: {!r}'.format(scope_name))
scope = self._scopes_by_name[scope_name][-1]

_logger.trace("Adding cleanup to scope {}: {}", scope, added)
if scope is None:
self._pending.append(added)
else:
Expand Down Expand Up @@ -120,7 +121,7 @@ def pop_scope(self, scope_name):

_logger.trace('CleanupManager: popping scope {0!r} (failure: {1}, interrupt: {2})', scope_name, in_failure, in_interruption)
scope = self._scope_stack[-1]
assert scope.name == scope_name, 'Attempted to pop scope {0!r}, but current scope is {1!r}'.format(scope_name, scope.name)
assert scope.name == scope_name, 'Attempted to pop scope {!r}, but current scope is {!r}'.format(scope_name, scope.name)
try:
self.call_cleanups(
scope=scope,
Expand Down Expand Up @@ -175,7 +176,7 @@ def __call__(self):
raise

def __repr__(self):
return "{0} ({1},{2})".format(self.func, self.args, self.kwargs)
return "{} ({},{})".format(self.func, self.args, self.kwargs)


class _Scope(object):
Expand All @@ -184,3 +185,6 @@ def __init__(self, name):
super(_Scope, self).__init__()
self.name = name
self.cleanups = []

def __repr__(self):
return "<Scope: {}>".format(self.name)
2 changes: 1 addition & 1 deletion slash/core/details.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def append(self, key, value):
"""
lst = self._details.setdefault(key, [])
if not isinstance(lst, list):
raise TypeError('Cannot append value to a {0.__class__.__name__!r} value'.format(lst))
raise TypeError('Cannot append value to a {.__class__.__name__!r} value'.format(lst))
lst.append(value)
if self._set_callback is not None:
self._set_callback(key, lst)
Expand Down
6 changes: 3 additions & 3 deletions slash/core/error.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ def func_name(self):
return self.traceback.cause.func_name

def __repr__(self):
return self.message
return '<{0.__class__.__name__}: {0.message}>'.format(self)

def get_detailed_traceback_str(self):
"""Returns a formatted traceback string for the exception caught
Expand All @@ -143,12 +143,12 @@ def get_detailed_traceback_str(self):
if index == 0:
f.writeln(title)
f.indent()
f.writeln(' - {0}: {1}'.format(var_name, var_repr['value']))
f.writeln(' - {}: {}'.format(var_name, var_repr['value']))
f.dedent()
self._cached_detailed_traceback_str = stream.getvalue()

return self._cached_detailed_traceback_str

def get_detailed_str(self):
return '{0}*** {1}'.format(
return '{}*** {}'.format(
self.get_detailed_traceback_str(), self)
47 changes: 31 additions & 16 deletions slash/core/exclusions.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,50 @@
from . import markers

from ..ctx import context

from .fixtures.parameters import Parametrization
from ..exceptions import UnknownFixtures
from .fixtures.parameters import Parametrization
from . import markers


def exclude(name, values):
def exclude(names, values):
"""
Excludes a specific parametrization of a test from running
:param name: can receive either a name of a parameter for this test, or a name of a fixture parameter
:param values: must be a list of values to exclude for the given parameter
"""
return markers.exclude_marker((name, values))
if not isinstance(values, (tuple, list)):
raise RuntimeError('Invalid exclude values specified: must be a sequence, got {!r}'.format(values))
if not isinstance(names, (tuple, list)):
names = (names,)
values = [(value,) for value in values]
elif not isinstance(values, (tuple, list)) or any(not isinstance(item, tuple) for item in values):
raise RuntimeError('Invalid exclude values specified for {}: {!r}'.format(', '.join(names), values))

values = [tuple(value_set) for value_set in values]
return markers.exclude_marker((names, values))

def is_excluded(test):
test_func = test.get_test_function()
exclusions = markers.exclude_marker.get_value(test_func, default=None)
if not exclusions:
return False
exclusions = dict(exclusions)
for parameter_name, values in exclusions.items():
param = context.session.fixture_store.resolve_name(parameter_name, start_point=test_func, namespace=test.get_fixture_namespace())
if not isinstance(param, Parametrization):
raise UnknownFixtures('{!r} is not a parameter, and therefore cannot be the base for value exclusions'.format(parameter_name))
try:
param_index = test.__slash__.variation.param_value_indices[param.info.id] #pylint: disable=no-member
except LookupError:
raise UnknownFixtures('{!r} cannot be excluded for {!r}'.format(parameter_name, test))
value = param.get_value_by_index(param_index)
if value in values:
for parameter_names, value_sets in exclusions.items():
params = []
values = []
for parameter_name in parameter_names:
param = context.session.fixture_store.resolve_name(parameter_name, start_point=test_func, namespace=test.get_fixture_namespace())
if not isinstance(param, Parametrization):
raise UnknownFixtures('{!r} is not a parameter, and therefore cannot be the base for value exclusions'.format(parameter_name))
params.append(param)


try:
param_index = test.__slash__.variation.param_value_indices[param.info.id] #pylint: disable=no-member
except LookupError:
raise UnknownFixtures('{!r} cannot be excluded for {!r}'.format(parameter_name, test))
value = param.get_value_by_index(param_index)
values.append(value)

if tuple(values) in value_sets:
return True
return False
Loading

0 comments on commit aa3af1b

Please sign in to comment.