From 9983b3a76f3d1fdc6739564a2ba054c88d36db34 Mon Sep 17 00:00:00 2001 From: Justin Shacklette Date: Sat, 30 Jul 2016 09:06:11 -0600 Subject: [PATCH 1/4] rename assert_soft() to assert_warn() --- README.md | 30 ++++++++++---------- assertpy/__init__.py | 2 +- assertpy/assertpy.py | 10 +++---- tests/test_readme.py | 22 +++++++-------- tests/{test_soft.py => test_warn.py} | 42 ++++++++++++++-------------- 5 files changed, 53 insertions(+), 53 deletions(-) rename tests/{test_soft.py => test_warn.py} (76%) diff --git a/README.md b/README.md index 91b1eb5..99edf06 100644 --- a/README.md +++ b/README.md @@ -282,7 +282,7 @@ Fluent assertions against the value of a given key can be done by prepending `ha ```py fred = {'first_name': 'Fred', 'last_name': 'Smith', 'shoe_size': 12} - + assert_that(fred).has_first_name('Fred') assert_that(fred).has_last_name('Smith') assert_that(fred).has_shoe_size(12) @@ -534,7 +534,7 @@ As noted above, dynamic assertions also work on dicts: ```py fred = {'first_name': 'Fred', 'last_name': 'Smith'} - + assert_that(fred).has_first_name('Fred') assert_that(fred).has_last_name('Smith') ``` @@ -613,24 +613,24 @@ Expected <3> to be equal to <2>, but was not. The `described_as()` helper causes the custom message `adding stuff` to be prepended to the front of the second error. -#### Soft Assertions +#### Just A Warning -There are times when you don't want to a test to fail at all, instead you only want a warning message. In this case, just replace `assert_that` with `assert_soft`. +There are times when you only want a warning message instead of an failing test. In this case, just replace `assert_that` with `assert_warn`. ```py -assert_soft('foo').is_length(4) -assert_soft('foo').is_empty() -assert_soft('foo').is_false() -assert_soft('foo').is_digit() -assert_soft('123').is_alpha() -assert_soft('foo').is_upper() -assert_soft('FOO').is_lower() -assert_soft('foo').is_equal_to('bar') -assert_soft('foo').is_not_equal_to('foo') -assert_soft('foo').is_equal_to_ignoring_case('BAR') +assert_warn('foo').is_length(4) +assert_warn('foo').is_empty() +assert_warn('foo').is_false() +assert_warn('foo').is_digit() +assert_warn('123').is_alpha() +assert_warn('foo').is_upper() +assert_warn('FOO').is_lower() +assert_warn('foo').is_equal_to('bar') +assert_warn('foo').is_not_equal_to('foo') +assert_warn('foo').is_equal_to_ignoring_case('BAR') ``` -The above soft assertions print the following warning messages (but an `AssertionError` is never raised): +The above assertions just print the following warning messages, and an `AssertionError` is never raised: ``` Expected to be of length <4>, but was <3>. diff --git a/assertpy/__init__.py b/assertpy/__init__.py index 385ab52..1e805fd 100644 --- a/assertpy/__init__.py +++ b/assertpy/__init__.py @@ -1,2 +1,2 @@ from __future__ import absolute_import -from .assertpy import assert_that, assert_soft, contents_of, fail, __version__ +from .assertpy import assert_that, assert_warn, contents_of, fail, __version__ diff --git a/assertpy/assertpy.py b/assertpy/assertpy.py index 462eee4..4017550 100644 --- a/assertpy/assertpy.py +++ b/assertpy/assertpy.py @@ -52,9 +52,9 @@ def assert_that(val, description=''): """Factory method for the assertion builder with value to be tested and optional description.""" return AssertionBuilder(val, description) -def assert_soft(val, description=''): +def assert_warn(val, description=''): """Factory method for the assertion builder with value to be tested, optional description, and - just print assertion failures, don't raise exceptions.""" + just warn on assertion failures instead of raisings exceptions.""" return AssertionBuilder(val, description, True) def contents_of(f, encoding='utf-8'): @@ -99,11 +99,11 @@ def fail(msg=''): class AssertionBuilder(object): """Assertion builder.""" - def __init__(self, val, description, soft=False, expected=None): + def __init__(self, val, description, warn=False, expected=None): """Construct the assertion builder.""" self.val = val self.description = description - self.soft = soft + self.warn = warn self.expected = expected def described_as(self, description): @@ -908,7 +908,7 @@ def when_called_with(self, *some_args, **some_kwargs): def _err(self, msg): """Helper to raise an AssertionError, and optionally prepend custom description.""" out = '%s%s' % ('[%s] ' % self.description if len(self.description) > 0 else '', msg) - if self.soft: + if self.warn: print(out) return self else: diff --git a/tests/test_readme.py b/tests/test_readme.py index 2ad4554..2179166 100644 --- a/tests/test_readme.py +++ b/tests/test_readme.py @@ -29,7 +29,7 @@ import sys import os import datetime -from assertpy import assert_that, assert_soft, contents_of, fail +from assertpy import assert_that, assert_warn, contents_of, fail class TestReadme(object): @@ -382,16 +382,16 @@ def test_custom_error_message(self): assert_that(str(e)).is_equal_to('[adding stuff] Expected <3> to be equal to <2>, but was not.') def test_soft_assertions(self): - assert_soft('foo').is_length(4) - assert_soft('foo').is_empty() - assert_soft('foo').is_false() - assert_soft('foo').is_digit() - assert_soft('123').is_alpha() - assert_soft('foo').is_upper() - assert_soft('FOO').is_lower() - assert_soft('foo').is_equal_to('bar') - assert_soft('foo').is_not_equal_to('foo') - assert_soft('foo').is_equal_to_ignoring_case('BAR') + assert_warn('foo').is_length(4) + assert_warn('foo').is_empty() + assert_warn('foo').is_false() + assert_warn('foo').is_digit() + assert_warn('123').is_alpha() + assert_warn('foo').is_upper() + assert_warn('FOO').is_lower() + assert_warn('foo').is_equal_to('bar') + assert_warn('foo').is_not_equal_to('foo') + assert_warn('foo').is_equal_to_ignoring_case('BAR') def test_chaining(self): fred = Person('Fred','Smith') diff --git a/tests/test_soft.py b/tests/test_warn.py similarity index 76% rename from tests/test_soft.py rename to tests/test_warn.py index 8731c99..6de80c2 100644 --- a/tests/test_soft.py +++ b/tests/test_warn.py @@ -28,21 +28,21 @@ import sys -from assertpy import assert_that, assert_soft, fail +from assertpy import assert_that, assert_warn, fail class TestSoft(object): def test_success(self): - assert_soft('foo').is_length(3) - assert_soft('foo').is_not_empty() - assert_soft('foo').is_true() - assert_soft('foo').is_alpha() - assert_soft('123').is_digit() - assert_soft('foo').is_lower() - assert_soft('FOO').is_upper() - assert_soft('foo').is_equal_to('foo') - assert_soft('foo').is_not_equal_to('bar') - assert_soft('foo').is_equal_to_ignoring_case('FOO') + assert_warn('foo').is_length(3) + assert_warn('foo').is_not_empty() + assert_warn('foo').is_true() + assert_warn('foo').is_alpha() + assert_warn('123').is_digit() + assert_warn('foo').is_lower() + assert_warn('FOO').is_upper() + assert_warn('foo').is_equal_to('foo') + assert_warn('foo').is_not_equal_to('bar') + assert_warn('foo').is_equal_to_ignoring_case('FOO') def test_failures(self): if sys.version_info[0] == 3: @@ -54,16 +54,16 @@ def test_failures(self): old = sys.stdout sys.stdout = StringIO() - assert_soft('foo').is_length(4) - assert_soft('foo').is_empty() - assert_soft('foo').is_false() - assert_soft('foo').is_digit() - assert_soft('123').is_alpha() - assert_soft('foo').is_upper() - assert_soft('FOO').is_lower() - assert_soft('foo').is_equal_to('bar') - assert_soft('foo').is_not_equal_to('foo') - assert_soft('foo').is_equal_to_ignoring_case('BAR') + assert_warn('foo').is_length(4) + assert_warn('foo').is_empty() + assert_warn('foo').is_false() + assert_warn('foo').is_digit() + assert_warn('123').is_alpha() + assert_warn('foo').is_upper() + assert_warn('FOO').is_lower() + assert_warn('foo').is_equal_to('bar') + assert_warn('foo').is_not_equal_to('foo') + assert_warn('foo').is_equal_to_ignoring_case('BAR') # stop capturing stdout out = sys.stdout.getvalue() From 2e6447c790393ac5f87263b348ad8e7f4feff7b3 Mon Sep 17 00:00:00 2001 From: Justin Shacklette Date: Sat, 30 Jul 2016 17:23:21 -0600 Subject: [PATCH 2/4] fixes #53 - correct impl of soft assertions --- assertpy/__init__.py | 2 +- assertpy/assertpy.py | 49 +++++++++++++++++++++++++++++++++++++------- 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/assertpy/__init__.py b/assertpy/__init__.py index 1e805fd..f673973 100644 --- a/assertpy/__init__.py +++ b/assertpy/__init__.py @@ -1,2 +1,2 @@ from __future__ import absolute_import -from .assertpy import assert_that, assert_warn, contents_of, fail, __version__ +from .assertpy import assert_that, assert_warn, soft_assertions, contents_of, fail, __version__ diff --git a/assertpy/assertpy.py b/assertpy/assertpy.py index 4017550..a1a644d 100644 --- a/assertpy/assertpy.py +++ b/assertpy/assertpy.py @@ -36,6 +36,7 @@ import numbers import collections import inspect +from contextlib import contextmanager __version__ = '0.9' @@ -48,14 +49,43 @@ xrange = xrange unicode = unicode + +### soft assertions ### +_soft_ctx = False +_soft_err = [] + +@contextmanager +def soft_assertions(): + global _soft_ctx + global _soft_err + + _soft_ctx = True + _soft_err = [] + + yield + + if _soft_err: + out = 'soft assertion failures:' + for i,msg in enumerate(_soft_err): + out += '\n%d. %s' % (i+1, msg) + raise AssertionError(out) + + _soft_err = [] + _soft_ctx = False + + +### factory methods ### def assert_that(val, description=''): """Factory method for the assertion builder with value to be tested and optional description.""" + global _soft_ctx + if _soft_ctx: + return AssertionBuilder(val, description, 'soft') return AssertionBuilder(val, description) def assert_warn(val, description=''): """Factory method for the assertion builder with value to be tested, optional description, and just warn on assertion failures instead of raisings exceptions.""" - return AssertionBuilder(val, description, True) + return AssertionBuilder(val, description, 'warn') def contents_of(f, encoding='utf-8'): """Helper to read the contents of the given file or path into a string with the given encoding. @@ -96,14 +126,15 @@ def fail(msg=''): else: raise AssertionError('Fail: %s!' % msg) + class AssertionBuilder(object): """Assertion builder.""" - def __init__(self, val, description, warn=False, expected=None): + def __init__(self, val, description='', kind=None, expected=None): """Construct the assertion builder.""" self.val = val self.description = description - self.warn = warn + self.kind = kind self.expected = expected def described_as(self, description): @@ -833,7 +864,7 @@ def extracting(self, *names): else: raise ValueError('val does not have property or zero-arg method <%s>' % name) extracted.append(tuple(items) if len(items) > 1 else items[0]) - return AssertionBuilder(extracted, self.description) + return AssertionBuilder(extracted, self.description, self.kind) ### dynamic assertions ### def __getattr__(self, attr): @@ -878,7 +909,7 @@ def raises(self, ex): raise TypeError('val must be function') if not issubclass(ex, BaseException): raise TypeError('given arg must be exception') - return AssertionBuilder(self.val, self.description, expected=ex) + return AssertionBuilder(self.val, self.description, self.kind, ex) def when_called_with(self, *some_args, **some_kwargs): """Asserts the val function when invoked with the given args and kwargs raises the expected exception.""" @@ -889,7 +920,7 @@ def when_called_with(self, *some_args, **some_kwargs): except BaseException as e: if issubclass(type(e), self.expected): # chain on with exception message as val - return AssertionBuilder(str(e), self.description) + return AssertionBuilder(str(e), self.description, self.kind) else: # got exception, but wrong type, so raise self._err('Expected <%s> to raise <%s> when called with (%s), but raised <%s>.' % ( @@ -908,9 +939,13 @@ def when_called_with(self, *some_args, **some_kwargs): def _err(self, msg): """Helper to raise an AssertionError, and optionally prepend custom description.""" out = '%s%s' % ('[%s] ' % self.description if len(self.description) > 0 else '', msg) - if self.warn: + if self.kind == 'warn': print(out) return self + elif self.kind == 'soft': + global _soft_err + _soft_err.append(out) + return self else: raise AssertionError(out) From 27a4a660da52a44b619540c3eaf29e5eaaef1538 Mon Sep 17 00:00:00 2001 From: Justin Shacklette Date: Sat, 30 Jul 2016 17:23:32 -0600 Subject: [PATCH 3/4] tests for soft assertions --- tests/test_soft.py | 116 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 tests/test_soft.py diff --git a/tests/test_soft.py b/tests/test_soft.py new file mode 100644 index 0000000..a32dcdc --- /dev/null +++ b/tests/test_soft.py @@ -0,0 +1,116 @@ +# Copyright (c) 2015-2016, Activision Publishing, Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import sys + +from assertpy import assert_that, soft_assertions, fail + +def test_success(): + with soft_assertions(): + assert_that('foo').is_length(3) + assert_that('foo').is_not_empty() + assert_that('foo').is_true() + assert_that('foo').is_alpha() + assert_that('123').is_digit() + assert_that('foo').is_lower() + assert_that('FOO').is_upper() + assert_that('foo').is_equal_to('foo') + assert_that('foo').is_not_equal_to('bar') + assert_that('foo').is_equal_to_ignoring_case('FOO') + +def test_failure(): + try: + with soft_assertions(): + assert_that('foo').is_length(4) + assert_that('foo').is_empty() + assert_that('foo').is_false() + assert_that('foo').is_digit() + assert_that('123').is_alpha() + assert_that('foo').is_upper() + assert_that('FOO').is_lower() + assert_that('foo').is_equal_to('bar') + assert_that('foo').is_not_equal_to('foo') + assert_that('foo').is_equal_to_ignoring_case('BAR') + fail('should have raised error') + except AssertionError as e: + out = str(e) + assert_that(out).contains('Expected to be of length <4>, but was <3>.') + assert_that(out).contains('Expected to be empty string, but was not.') + assert_that(out).contains('Expected , but was not.') + assert_that(out).contains('Expected to contain only digits, but did not.') + assert_that(out).contains('Expected <123> to contain only alphabetic chars, but did not.') + assert_that(out).contains('Expected to contain only uppercase chars, but did not.') + assert_that(out).contains('Expected to contain only lowercase chars, but did not.') + assert_that(out).contains('Expected to be equal to , but was not.') + assert_that(out).contains('Expected to be not equal to , but was.') + assert_that(out).contains('Expected to be case-insensitive equal to , but was not.') + +def test_failure_chain(): + try: + with soft_assertions(): + assert_that('foo').is_length(4).is_empty().is_false().is_digit().is_upper()\ + .is_equal_to('bar').is_not_equal_to('foo').is_equal_to_ignoring_case('BAR') + fail('should have raised error') + except AssertionError as e: + out = str(e) + assert_that(out).contains('Expected to be of length <4>, but was <3>.') + assert_that(out).contains('Expected to be empty string, but was not.') + assert_that(out).contains('Expected , but was not.') + assert_that(out).contains('Expected to contain only digits, but did not.') + assert_that(out).contains('Expected to contain only uppercase chars, but did not.') + assert_that(out).contains('Expected to be equal to , but was not.') + assert_that(out).contains('Expected to be not equal to , but was.') + assert_that(out).contains('Expected to be case-insensitive equal to , but was not.') + +def test_expected_exception_success(): + with soft_assertions(): + assert_that(func_err).raises(RuntimeError).when_called_with('foo').is_equal_to('err') + +def test_expected_exception_failure(): + try: + with soft_assertions(): + assert_that(func_err).raises(RuntimeError).when_called_with('foo').is_equal_to('bar') + assert_that(func_ok).raises(RuntimeError).when_called_with('baz') + fail('should have raised error') + except AssertionError as e: + out = str(e) + assert_that(out).contains('Expected to be equal to , but was not.') + assert_that(out).contains("Expected to raise when called with ('baz').") + +def test_tmp(): + with soft_assertions(): + assert_that('foo').is_length(4) + assert_that('foo').is_equal_to('bar') + assert_that(123).is_between(100, 200) + +def func_ok(arg): + pass + +def func_err(arg): + raise RuntimeError('err') + From 5e8805aff9f3a367a12e42e0209ba1a961bd6f74 Mon Sep 17 00:00:00 2001 From: Justin Shacklette Date: Sat, 30 Jul 2016 17:25:51 -0600 Subject: [PATCH 4/4] oops, remove debugging --- tests/test_soft.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/test_soft.py b/tests/test_soft.py index a32dcdc..ca39c30 100644 --- a/tests/test_soft.py +++ b/tests/test_soft.py @@ -102,12 +102,6 @@ def test_expected_exception_failure(): assert_that(out).contains('Expected to be equal to , but was not.') assert_that(out).contains("Expected to raise when called with ('baz').") -def test_tmp(): - with soft_assertions(): - assert_that('foo').is_length(4) - assert_that('foo').is_equal_to('bar') - assert_that(123).is_between(100, 200) - def func_ok(arg): pass