diff --git a/README.md b/README.md index 261a9e58..49d0fd70 100644 --- a/README.md +++ b/README.md @@ -178,12 +178,18 @@ ward --search "raises(ZeroDivisionError)" ward --search "@xfail" ``` -**To run a test called `test_the_sky_is_blue`:** +**Run a test called `test_the_sky_is_blue`:** ```text ward --search test_the_sky_is_blue ``` +**Run a test described as `"my_function should return False"`:** + +```text +ward --search "my_function should return False" +``` + **Running tests inside a module:** The search takes place on the fully qualified name, so you can run a single @@ -199,10 +205,10 @@ will also be selected and ran. This approach is useful for quickly querying tests and running those which match a simple query, making it useful for development. -Of course, sometimes you want to be very specific when declaring which tests to run. - #### Specific test selection +Sometimes you want to be very specific when declaring which tests to run. + Ward will provide an option to query tests on name and description using substring or regular expression matching. diff --git a/setup.py b/setup.py index 35877801..1d17b4c3 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup -version = "0.12.0a0" +version = "0.13.0a0" description = "A modern Python 3 test framework for finding and fixing flaws faster." with open("README.md", "r") as fh: if platform.system() != "Windows": diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py index cbde9474..9740360c 100644 --- a/tests/test_fixtures.py +++ b/tests/test_fixtures.py @@ -1,5 +1,5 @@ -from ward import expect, fixture, raises, test -from ward.fixtures import Fixture, FixtureExecutionError, FixtureCache +from ward import expect, fixture, test +from ward.fixtures import Fixture, FixtureCache @fixture @@ -7,56 +7,12 @@ def exception_raising_fixture(): def i_raise_an_exception(): raise ZeroDivisionError() - return Fixture(key="fix_a", fn=i_raise_an_exception) + return Fixture(fn=i_raise_an_exception) -@test("Fixture.resolve correctly recurses fixture tree, collecting dependencies") -def _(): - @fixture - def grandchild_a(): - return 1 - - @fixture - def child_b(): - return 1 - - @fixture - def child_a(grandchild_a=grandchild_a): - return grandchild_a + 1 - - @fixture - def parent(child_a=child_a, child_b=child_b): - return child_a + child_b + 1 - - grandchild_a_fix = Fixture(key="grandchild_a", fn=grandchild_a) - child_a_fix = Fixture(key="child_a", fn=child_a) - child_b_fix = Fixture(key="child_b", fn=child_b) - parent_fix = Fixture(key="fix_a", fn=parent) - - cache = FixtureCache() - cache.cache_fixtures((grandchild_a_fix, child_a_fix, child_b_fix, parent_fix)) - - resolved_parent = parent_fix.resolve(cache) - - # Each of the fixtures add 1, so the final value returned - # by the tree should be 4, since there are 4 fixtures. - expect(resolved_parent.resolved_val).equals(4) - - -@test("FixtureRegistry.cache_fixture can store and retrieve a single fixture") +@test("FixtureCache.cache_fixture can store and retrieve a single fixture") def _(f=exception_raising_fixture): cache = FixtureCache() cache.cache_fixture(f) expect(cache[f.key]).equals(f) - - -@test( - "FixtureRegistry.resolve raises FixtureExecutionError when fixture raises an exception" -) -def _(f=exception_raising_fixture): - cache = FixtureCache() - cache.cache_fixtures([f]) - - with raises(FixtureExecutionError): - f.resolve(cache) diff --git a/tests/test_suite.py b/tests/test_suite.py index 640dea7b..23448764 100644 --- a/tests/test_suite.py +++ b/tests/test_suite.py @@ -34,8 +34,8 @@ def a(b=b): @fixture def fixtures(a=fixture_a, b=fixture_b): return { - "fixture_a": Fixture(key="fixture_a", fn=a), - "fixture_b": Fixture(key="fixture_b", fn=b), + "fixture_a": Fixture(fn=a), + "fixture_b": Fixture(fn=b), } diff --git a/tests/test_testing.py b/tests/test_testing.py index 8a5990e1..d8d5b013 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -1,8 +1,8 @@ from unittest.mock import Mock -from ward import expect, fixture, test -from ward.fixtures import Fixture, FixtureCache -from ward.testing import Test +from ward import expect +from ward.fixtures import fixture +from ward.testing import Test, test def f(): @@ -68,23 +68,6 @@ def _(anonymous_test=anonymous_test): expect(anonymous_test.has_deps()).equals(False) -@test("Test.resolve_args should return {} when test doesn't use fixtures") -def _(anonymous_test=anonymous_test): - expect(anonymous_test.resolve_args(FixtureCache())).equals({}) - - -@test("Test.resolve_args should return a map of param names to resolved Fixture") -def _(): - reg = FixtureCache() - val = 1 - fixture = Fixture("my_fixture", lambda: val) - reg.cache_fixture(fixture) - test = Test(lambda my_fixture: 1, "module_name") - - expect(test.resolve_args(reg)).equals({"my_fixture": fixture}) - expect(fixture.resolved_val).equals(val) - - @test("Test.__call__ should delegate to the function it wraps") def _(): mock = Mock() diff --git a/ward/fixtures.py b/ward/fixtures.py index 87ae0a80..d1ad9a6c 100644 --- a/ward/fixtures.py +++ b/ward/fixtures.py @@ -1,7 +1,8 @@ import inspect from contextlib import suppress +from dataclasses import dataclass, field from functools import partial, wraps -from typing import Callable, Dict, Iterable +from typing import Callable, Dict from ward.models import WardMeta @@ -18,86 +19,42 @@ class FixtureExecutionError(Exception): pass +@dataclass class Fixture: - def __init__(self, key: str, fn: Callable): - self.key = key + def __init__(self, fn: Callable): self.fn = fn self.gen = None self.resolved_val = None - def deps(self): - return inspect.signature(self.fn).parameters + @property + def key(self): + path = inspect.getfile(fixture) + name = self.name + return f"{path}::{name}" + + @property + def name(self): + return self.fn.__name__ @property def is_generator_fixture(self): return inspect.isgeneratorfunction(inspect.unwrap(self.fn)) - def resolve(self, fix_cache) -> "Fixture": - """Traverse the fixture tree to resolve the value of this fixture""" - - # If this fixture has no children, cache and return the resolved value - if not self.deps(): - try: - if self.is_generator_fixture: - self.gen = self.fn() - self.resolved_val = next(self.gen) - else: - self.resolved_val = self.fn() - except Exception as e: - raise FixtureExecutionError( - f"Unable to execute fixture '{self.key}'" - ) from e - fix_cache.cache_fixture(self) - return self - - # Otherwise, we have to find the child fixture vals, and call self - children = self.deps() - children_resolved = [] - for child in children: - child_fixture = fix_cache[child].resolve(fix_cache) - children_resolved.append(child_fixture) - - # We've resolved the values of all child fixtures - try: - child_resolved_vals = [child.resolved_val for child in children_resolved] - if self.is_generator_fixture: - self.gen = self.fn(*child_resolved_vals) - self.resolved_val = next(self.gen) - else: - self.resolved_val = self.fn(*child_resolved_vals) - except Exception as e: - raise FixtureExecutionError( - f"Unable to execute fixture '{self.key}'" - ) from e - - fix_cache.cache_fixture(self) - return self + def deps(self): + return inspect.signature(self.fn).parameters def teardown(self): if self.is_generator_fixture: next(self.gen) +@dataclass class FixtureCache: - def __init__(self): - self._fixtures: Dict[str, Fixture] = {} - - def _get_fixture(self, fixture_name: str) -> Fixture: - try: - return self._fixtures[fixture_name] - except KeyError: - raise CollectionError(f"Couldn't find fixture '{fixture_name}'.") + _fixtures: Dict[str, Fixture] = field(default_factory=dict) def cache_fixture(self, fixture: Fixture): - """Update the fixture in the cache, for example, replace it with its resolved analogue""" - # TODO: Caching can be used to implement fixture scoping, - # but currently resolved cached fixtures aren't used. self._fixtures[fixture.key] = fixture - def cache_fixtures(self, fixtures: Iterable[Fixture]): - for fixture in fixtures: - self.cache_fixture(fixture) - def teardown_all(self): """Run the teardown code for all generator fixtures in the cache""" for fixture in self._fixtures.values(): @@ -110,12 +67,6 @@ def __contains__(self, key: str): def __getitem__(self, item): return self._fixtures[item] - def __len__(self): - return len(self._fixtures) - - -fixture_cache = FixtureCache() - def fixture(func=None, *, description=None): if func is None: @@ -134,9 +85,3 @@ def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper - - -def get_cache_key_for_func(fixture: Callable): - path = inspect.getfile(fixture) - name = fixture.__name__ - return f"{path}::{name}" diff --git a/ward/run.py b/ward/run.py index 7ff5deef..c77f3c2d 100644 --- a/ward/run.py +++ b/ward/run.py @@ -10,7 +10,6 @@ load_modules, search_generally, ) -from ward.fixtures import fixture_cache from ward.suite import Suite from ward.terminal import SimpleTestResultWrite from ward.util import get_exit_code diff --git a/ward/terminal.py b/ward/terminal.py index 48a5020b..ce4760f7 100644 --- a/ward/terminal.py +++ b/ward/terminal.py @@ -147,11 +147,6 @@ def output_single_test_result(self, test_result: TestResult): def output_why_test_failed_header(self, test_result: TestResult): test = test_result.test - params_list = ", ".join(lightblack(str(v)) for v in test.deps().keys()) - if test.has_deps(): - test_name_suffix = f"({params_list})" - else: - test_name_suffix = "" if test.description: name_or_desc = ( @@ -164,7 +159,6 @@ def output_why_test_failed_header(self, test_result: TestResult): colored(" Failure", color="red"), "in", colored(name_or_desc, attrs=["bold"]), - test_name_suffix, "\n", ) diff --git a/ward/testing.py b/ward/testing.py index 63f1420b..68d00c19 100644 --- a/ward/testing.py +++ b/ward/testing.py @@ -9,7 +9,6 @@ Fixture, FixtureCache, FixtureExecutionError, - get_cache_key_for_func, ) from ward.models import Marker, SkipMarker, XfailMarker, WardMeta @@ -86,26 +85,11 @@ def deps(self) -> MappingProxyType: def has_deps(self) -> bool: return len(self.deps()) > 0 - def resolve_args(self, fixture_cache: FixtureCache) -> Dict[str, Fixture]: - """Resolve fixture that has been injected into this test""" - if not self.has_deps(): - return {} - - # Construct a dict of kwargs to pass into the test when it's called - resolved_args = {} - for fixture_name in self.deps(): - fixture = fixture_cache[fixture_name] - resolved_arg = fixture.resolve(fixture_cache) - resolved_args[fixture_name] = resolved_arg - - return resolved_args - def resolve_fixtures(self) -> Dict[str, Fixture]: """ - Resolve fixtures and return the resultant BoundArguments - formed by partially binding resolved fixture values. + Resolve fixtures and return the resultant name -> Fixture dict. Resolved values will be stored in fixture_cache, accessible - using the fixture cache key (See `fixtures.get_cache_key_for_func`). + using the fixture cache key (See `Fixture.key`). """ signature = inspect.signature(self.fn) default_binding = signature.bind_partial() @@ -123,30 +107,29 @@ def resolve_fixtures(self) -> Dict[str, Fixture]: resolved_args[name] = resolved return resolved_args - def _resolve_single_fixture(self, fixture: Callable) -> Fixture: - key = get_cache_key_for_func(fixture) - if key in self.fixture_cache: - return self.fixture_cache[key] + def _resolve_single_fixture(self, fixture_fn: Callable) -> Fixture: + fixture = Fixture(fixture_fn) + if fixture.key in self.fixture_cache: + return self.fixture_cache[fixture.key] - deps = inspect.signature(fixture) + deps = inspect.signature(fixture_fn) has_deps = len(deps.parameters) > 0 - f = Fixture(key, fixture) - is_generator = inspect.isgeneratorfunction(inspect.unwrap(fixture)) + is_generator = inspect.isgeneratorfunction(inspect.unwrap(fixture_fn)) if not has_deps: try: if is_generator: - f.gen = fixture() - f.resolved_val = next(f.gen) + fixture.gen = fixture_fn() + fixture.resolved_val = next(fixture.gen) else: - f.resolved_val = fixture() + fixture.resolved_val = fixture_fn() except Exception as e: raise FixtureExecutionError( - f"Unable to execute fixture '{f.key}'" + f"Unable to execute fixture '{fixture.name}'" ) from e - self.fixture_cache.cache_fixture(f) - return f + self.fixture_cache.cache_fixture(fixture) + return fixture - signature = inspect.signature(fixture) + signature = inspect.signature(fixture_fn) children_defaults = signature.bind_partial() children_defaults.apply_defaults() children_resolved = {} @@ -155,16 +138,16 @@ def _resolve_single_fixture(self, fixture: Callable) -> Fixture: children_resolved[name] = child_resolved try: if is_generator: - f.gen = fixture(**self._resolve_fixture_values(children_resolved)) - f.resolved_val = next(f.gen) + fixture.gen = fixture_fn(**self._resolve_fixture_values(children_resolved)) + fixture.resolved_val = next(fixture.gen) else: - f.resolved_val = fixture( + fixture.resolved_val = fixture_fn( **self._resolve_fixture_values(children_resolved) ) except Exception as e: - raise FixtureExecutionError(f"Unable to execute fixture '{f.key}'") from e - self.fixture_cache.cache_fixture(f) - return f + raise FixtureExecutionError(f"Unable to execute fixture '{fixture.name}'") from e + self.fixture_cache.cache_fixture(fixture) + return fixture def _resolve_fixture_values( self, fixture_dict: Dict[str, Fixture]