Skip to content
This repository has been archived by the owner on Oct 1, 2024. It is now read-only.

Commit

Permalink
Merge pull request #51 from darrenburns/fixture-cleanup
Browse files Browse the repository at this point in the history
Fixture cleanup
  • Loading branch information
darrenburns authored Oct 30, 2019
2 parents ef2cac9 + 7fe4da3 commit 9f67369
Show file tree
Hide file tree
Showing 9 changed files with 57 additions and 191 deletions.
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down
52 changes: 4 additions & 48 deletions tests/test_fixtures.py
Original file line number Diff line number Diff line change
@@ -1,62 +1,18 @@
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
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)
4 changes: 2 additions & 2 deletions tests/test_suite.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}


Expand Down
23 changes: 3 additions & 20 deletions tests/test_testing.py
Original file line number Diff line number Diff line change
@@ -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():
Expand Down Expand Up @@ -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()
Expand Down
89 changes: 17 additions & 72 deletions ward/fixtures.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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():
Expand All @@ -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:
Expand All @@ -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}"
1 change: 0 additions & 1 deletion ward/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 0 additions & 6 deletions ward/terminal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
Expand All @@ -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",
)

Expand Down
Loading

0 comments on commit 9f67369

Please sign in to comment.