From 28be05d3d2b170114b735e540f05a58a7d2b5862 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Falc=C3=A3o?= Date: Sun, 21 Jan 2024 06:28:00 +0000 Subject: [PATCH] more test coverage] --- Makefile | 2 - docs/source/changelog.rst | 5 +- setup.cfg | 2 +- sure/original.py | 54 +----------------- tests/test_original_api.py | 105 +++++++++++++++++++---------------- tests/unit/test_astuneval.py | 44 ++++++++++----- 6 files changed, 95 insertions(+), 117 deletions(-) diff --git a/Makefile b/Makefile index 635e2cb..fb8c610 100644 --- a/Makefile +++ b/Makefile @@ -55,13 +55,11 @@ docs: html-docs $(OPEN_COMMAND) docs/build/html/index.html test tests: - @$(VENV)/bin/pytest --cov=sure tests/unit/test_astuneval.py @$(VENV)/bin/pytest --cov=sure tests # runs main command-line tool run: | $(LIBEXEC_PATH) $(LIBEXEC_PATH) --reap-warnings tests/crashes - $(LIBEXEC_PATH) --reap-warnings --special-syntax --with-coverage --cover-branches --cover-erase --cover-module=sure.core --cover-module=sure tests/runner $(LIBEXEC_PATH) --reap-warnings --special-syntax --with-coverage --cover-branches --cover-erase --cover-module=sure --immediate --cover-module=sure --ignore tests/crashes tests push-release: dist # pushes distribution tarballs of the current version diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index b4b9cdc..5fc5003 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -38,7 +38,10 @@ v3.0.0 - :meth:`sure.original.AssertionHelper.differs` - :meth:`sure.original.AssertionHelper.has` - :meth:`sure.original.AssertionHelper.is_a` - + - :meth:`sure.original.AssertionHelper.every_item_is` + - :meth:`sure.original.AssertionHelper.at` + - :meth:`sure.original.AssertionHelper.like` + - Feel free to open an issue requesting any of those methods to be added back to Sure's codebase. [v2.0.0] -------- diff --git a/setup.cfg b/setup.cfg index 7aa5aa0..0eea7d1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [tool:pytest] -addopts = --cov=sure --ignore tests/crashes -v --capture=no --disable-warnings --maxfail=1 +addopts = --cov=sure --ignore tests/crashes -v --capture=no --disable-warnings testpaths = tests filterwarnings = diff --git a/sure/original.py b/sure/original.py index 6335b33..9d95283 100644 --- a/sure/original.py +++ b/sure/original.py @@ -82,7 +82,7 @@ def __init__(self, src, if all_integers(within_range): if len(within_range) != 2: raise TypeError( - 'within_range parameter must be a tuple with 2 objects', + f"within_range parameter must be a tuple with 2 objects, received a `{type(within_range).__name__}' with {len(within_range)} objects instead", ) self._range = within_range @@ -115,7 +115,7 @@ def match(self, *args, **kw): def raises(self, exc, msg=None): if not callable(self.actual): - raise TypeError(f'{self.actual} is not callable') + raise TypeError(f'{repr(self.actual)} is not callable') try: self.actual(*self._callable_args, **self._callable_kw) @@ -147,18 +147,6 @@ def raises(self, exc, msg=None): f'Expected to match regex: {repr(msg.pattern)}\n against:\n {repr(str(err))}' ) - elif isinstance(msg, (str, )) and msg not in str(err): - raise AssertionError( - 'When calling %r the exception message does not match. ' \ - 'Expected: %r\n got:\n %r' % (self.actual, msg, err) - ) - - elif isinstance(msg, re.Pattern) and not msg.search(err): - raise AssertionError( - 'When calling %r the exception message does not match. ' \ - 'Expected to match regex: %r\n against:\n %r' % (identify_caller_location(self.actual), msg.pattern, err) - ) - else: raise e else: @@ -177,12 +165,7 @@ def raises(self, exc, msg=None): self._callable_kw, exc)) else: raise AssertionError( - 'at %s:\ncalling %s() with args %r and kws %r did not raise %r' % ( - _src_filename, - self.actual.__name__, - self._callable_args, - self._callable_kw, exc - ) + f'at {_src_filename}:\ncalling {self.actual.__name__}() with args {repr(self._callable_args)} and kws {repr(self._callable_kw)} did not raise {repr(exc)}' ) return True @@ -215,7 +198,6 @@ def equals(self, expectation): return True def looks_like(self, expectation): - comp = DeepComparison(self.actual, expectation) old_src = pformat(self.actual) old_dst = pformat(expectation) self.actual = re.sub(r'\s', '', self.actual).lower() @@ -226,29 +208,6 @@ def looks_like(self, expectation): else: raise AssertionError(error) - def every_item_is(self, expectation): - msg = 'all members of %r should be %r, but the %dth is %r' - for index, item in enumerate(self.actual): - if self._range: - if index < self._range[0] or index > self._range[1]: - continue - - error = msg % (self.actual, expectation, index, item) - if item != expectation: - raise AssertionError(error) - - return True - - def at(self, key): - if not self.has(key): - raise AssertionError(f"key {key} not present in {self.actual}") - - if isinstance(self.actual, dict): - return AssertionHelper(self.actual[key]) - - else: - return AssertionHelper(getattr(self.actual, key)) - def _get_int_or_length(self, obj: Union[int, typing.Iterable]): if isinstance(obj, Iterable): return len(obj) @@ -344,9 +303,6 @@ def len_is_not(self, that: Union[int, typing.Iterable]): return True - def like(self, that): - return self.has(that) - def the_attribute(self, attr): self._attribute = attr return self @@ -375,10 +331,6 @@ def matches(self, items): ) for index, (item, other) in enumerate(zip(self.actual, items)): - if self._range: - if index < self._range[0] or index > self._range[1]: - continue - value = get_eval(item) error = msg % (self.actual, index, self.__element_access_expr__, other, value) diff --git a/tests/test_original_api.py b/tests/test_original_api.py index 17246bd..acacb21 100644 --- a/tests/test_original_api.py +++ b/tests/test_original_api.py @@ -17,9 +17,7 @@ import os import sure import time - from datetime import datetime - from sure import that, this from sure import expects from sure import action_for @@ -27,10 +25,11 @@ from sure import within from sure import second, miliseconds from sure import StagingArea +from sure.doubles import Dummy, anything from sure.errors import WrongUsageError from sure.special import is_cpython from sure.loader import collapse_path -from sure.original import all_integers +from sure.original import all_integers, AssertionHelper def test_setup_with_context(): @@ -63,7 +62,7 @@ def it_crashes(): assert that(it_crashes).raises( TypeError, ( - "the function it_crashes defined at tests/test_original_api.py line 60, is being " + "the function it_crashes defined at tests/test_original_api.py line 59, is being " "decorated by either @that_with_context or @scenario, so it should " "take at least 1 parameter, which is the test context" ), @@ -267,28 +266,11 @@ def __repr__(self): assert that(shapes, within_range=(1, 2)).the_attribute("name").equals("square") -def test_that_checking_all_elements(): - "that(iterable).every_item_is('value')" - shapes = [ - "cube", - "ball", - "ball", - "piramid", - ] - - assert shapes[0] != "ball" - assert shapes[3] != "ball" - - assert shapes[1] == "ball" - assert shapes[2] == "ball" - - assert that(shapes, within_range=(1, 2)).every_item_is("ball") - - def test_that_checking_each_matches(): "that(iterable).in_each('').equals('value')" class animal(object): + def __init__(self, kind): self.attributes = { "class": "mammal", @@ -305,7 +287,6 @@ def __init__(self, kind): assert animals[0].attributes["kind"] != "cow" assert animals[1].attributes["kind"] != "cow" - assert animals[2].attributes["kind"] == "cow" assert animals[3].attributes["kind"] == "cow" assert animals[4].attributes["kind"] == "cow" @@ -329,17 +310,9 @@ def __init__(self, kind): .matches(["dog", "cat", "cow", "cow", "cow"]) ) - try: - assert that(animals).in_each("attributes['kind']").matches(["dog"]) - assert False, "should not reach here" - except AssertionError as e: - assert that(str(e)).equals( - "%r has 5 items, but the matching list has 1: %r" - % ( - ["dog", "cat", "cow", "cow", "cow"], - ["dog"], - ) - ) + expects(that(animals).in_each("attributes['kind']").matches).when.called_with(["dog"]).should.have.raised( + f"{repr(['dog', 'cat', 'cow', 'cow', 'cow'])} has 5 items, but the matching list has 1: {repr(['dog'])}" + ) def test_that_raises(): @@ -587,20 +560,12 @@ def test_within_pass(): def test_within_five_milicesonds_fails_when_function_takes_six_miliseconds(): "within(five=miliseconds) should fail when the decorated function takes six miliseconds to run" - def sleepy(*a): time.sleep(0.6) - failed = False - try: - within(five=miliseconds)(sleepy)() - except AssertionError as e: - failed = True - expects( - "sleepy [tests/test_original_api.py line 591] did not run within five miliseconds" - ).to.equal(str(e)) - - assert failed, "within(five=miliseconds)(sleepy) did not fail" + expects(within(five=miliseconds)(sleepy)).when.called.to.have.raised( + "sleepy [tests/test_original_api.py line 563] did not run within five miliseconds" + ) def test_that_is_a_matcher_should_absorb_callables_to_be_used_as_matcher(): @@ -767,7 +732,7 @@ def test_depends_on_failing_due_to_lack_of_attribute_in_context(): fullpath = collapse_path(os.path.abspath(__file__)) error = ( - f'the action "variant_action" defined at {fullpath}:776 ' + f'the action "variant_action" defined at {fullpath}:741 ' 'depends on the attribute "data_structure" to be available in the' " current context" ) @@ -789,11 +754,12 @@ def test_depends_on_failing_due_not_calling_a_previous_action(): "it fails when an action depends on some attribute that is being " "provided by other actions" fullpath = collapse_path(os.path.abspath(__file__)) + error = ( - 'the action "my_action" defined at {0}:804 ' + 'the action "my_action" defined at {0}:770 ' 'depends on the attribute "some_attr" to be available in the context.' " Perhaps one of the following actions might provide that attribute:\n" - " -> dependency_action at {0}:800".replace("{0}", fullpath) + " -> dependency_action at {0}:766".replace("{0}", fullpath) ) def with_setup(context): @@ -1635,3 +1601,46 @@ def test_within_wrong_usage(): WrongUsageError, "within() takes a single keyword argument where the argument must be a numerical description from one to eighteen and the value. For example: within(eighteen=miliseconds)", ) + + +def test_assertion_helper_within_range_wrong_number_of_elements(): + expects(AssertionHelper).when.called_with(object, within_range=set(range(3))).should.have.raised( + TypeError, + "within_range parameter must be a tuple with 2 objects, received a `set' with 3 objects instead" + ) + + +def test_assertion_helper_with_kws(): + src = Dummy('assertion_helper.src') + assertion_helper = AssertionHelper(src, with_args=("z", "y"), with_kws={"a": "b"}) + expects(assertion_helper).to.have.property("_callable_args").being.a(list) + expects(assertion_helper).to.have.property("_callable_args").being.equal(["z", "y"]) + expects(assertion_helper).to.have.property("_callable_kw").being.a(dict) + expects(assertion_helper).to.have.property("_callable_kw").being.equal({"a": "b"}) + expects(assertion_helper).to.have.property("src").being.a(Dummy) + expects(assertion_helper).to.have.property("src").being.equal(src) + + +def test_assertion_helper_raises_raises_type_error_noncallable(): + src = Dummy('assertion_helper.src') + assertion_helper = AssertionHelper(src) + expects(assertion_helper.raises).when.called_with("dummy").to.have.raised( + TypeError, + " is not callable" + ) + + +def test_assertion_helper_raises_fails_when_the_expected_error_does_not_happen_given_function(): + assertion_helper = AssertionHelper(lambda: None) + + expects(assertion_helper.raises).when.called_with("error").to.have.raised( + f'calling function ({collapse_path(__file__)} at line: "1634") with args [] and kws {{}} did not raise {repr("error")}' + ) + + +def test_assertion_helper_raises_fails_when_the_expected_error_does_not_happen_builtin_function(): + assertion_helper = AssertionHelper(vars) + + expects(assertion_helper.raises).when.called_with("error").to.have.raised( + "at :\ncalling vars() with args [] and kws {} did not raise 'error'" + ) diff --git a/tests/unit/test_astuneval.py b/tests/unit/test_astuneval.py index cbaca15..906a5b1 100644 --- a/tests/unit/test_astuneval.py +++ b/tests/unit/test_astuneval.py @@ -50,19 +50,35 @@ class MonacoGrandPrix1990: def test_parse_accessor_attr_accessor(): - class FirstResponder: - def __init__(self, bound: str, damage: str): - self.bound = bound - self.damage = damage - - class Incident: - first_responders = [ - FirstResponder("Wyckoff", "unknown"), - FirstResponder("Beth Israel", "unknown"), - FirstResponder("Brooklyn Hospital Center", "unknown"), - FirstResponder("Woodhull", "administered wrong medication"), + class Event: + def __init__(self, description: str): + self.tag = description + + class LogBook: + events = [ + Event("occurrenceA"), + Event("occurrenceB"), + Event("occurrenceC"), + Event("occurrenceD"), + Event("occurrenceE"), + Event("occurrenceF"), ] - expects(parse_accessor("first_responders[3].damage")).to.be.a(AttributeAccessor) - access_damage = parse_accessor("first_responders[3].damage") - expects(access_damage(Incident)).to.equal("administered wrong medication") + expects(parse_accessor("events[3].description")).to.be.a(AttributeAccessor) + + access_description = parse_accessor("events[3].tag") + expects(access_description(LogBook)).to.equal("occurrenceD") + + +def test_accessor_access_not_implemented(): + accessor = Accessor(parse_body("attribute")) + expects(accessor.access).when.called_with(object).to.throw( + NotImplementedError + ) + + +def test_parse_body_syntax_error(): + parse_body.when.called_with("substance = collect()\nsubstance.reuse()").to.throw( + SyntaxError, + "'substance = collect()\\nsubstance.reuse()' exceeds the maximum body count for ast nodes" + )