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 #161 from JoshKarpel/more-fixtures
Browse files Browse the repository at this point in the history
Add information from tests to `ward fixtures`
  • Loading branch information
darrenburns authored Jul 10, 2020
2 parents a77c137 + dfbd899 commit 67ff1bb
Show file tree
Hide file tree
Showing 12 changed files with 695 additions and 164 deletions.
34 changes: 32 additions & 2 deletions ward/collect.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,16 @@
Optional,
Set,
Tuple,
Iterator,
Collection,
)

from cucumber_tag_expressions.model import Expression

from ward.errors import CollectionError
from ward.models import WardMeta
from ward.testing import Test, anonymous_tests, is_test_module_name
from ward.fixtures import Fixture
from ward.util import get_absolute_path

Glob = str
Expand Down Expand Up @@ -137,9 +140,9 @@ def get_tests_in_modules(
)


def search_generally(
def filter_tests(
tests: Iterable[Test], query: str = "", tag_expr: Optional[Expression] = None,
) -> Generator[Test, None, None]:
) -> Iterator[Test]:
if not query and not tag_expr:
yield from tests

Expand All @@ -158,3 +161,30 @@ def search_generally(

if matches_query and matches_tags:
yield test


def filter_fixtures(
fixtures: Iterable[Fixture],
query: str = "",
paths: Optional[Collection[Path]] = None,
) -> Iterator[Fixture]:
if paths is None:
paths = []
paths = {path.absolute() for path in paths}

for fixture in fixtures:
matches_query = (
not query
or query in f"{fixture.module_name}."
or query in inspect.getsource(fixture.fn)
or query in fixture.qualified_name
)

matches_paths = (
not paths
or fixture.path in paths
or any(parent in paths for parent in fixture.path.parents)
)

if matches_query and matches_paths:
yield fixture
76 changes: 73 additions & 3 deletions ward/fixtures.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,23 @@
import asyncio
import collections
import inspect
from contextlib import suppress
from functools import partial, wraps
from pathlib import Path
from typing import Callable, Dict, Union, Optional, Any, Generator, AsyncGenerator
from typing import (
Callable,
Dict,
Union,
Optional,
Any,
Generator,
AsyncGenerator,
List,
Iterable,
Mapping,
Tuple,
Collection,
)

from dataclasses import dataclass, field

Expand All @@ -16,6 +30,16 @@ class Fixture:
gen: Union[Generator, AsyncGenerator] = None
resolved_val: Any = None

def __hash__(self):
return hash(self._id)

def __eq__(self, other):
return self._id == other._id

@property
def _id(self):
return self.__class__, self.name, self.path, self.line_number

@property
def key(self) -> str:
path = self.path
Expand All @@ -34,6 +58,15 @@ def name(self):
def path(self):
return self.fn.ward_meta.path

@property
def module_name(self):
return self.fn.__module__

@property
def qualified_name(self) -> str:
name = self.name or ""
return f"{self.module_name}.{name}"

@property
def line_number(self) -> int:
return inspect.getsourcelines(self.fn)[1]
Expand All @@ -53,6 +86,12 @@ def is_coroutine_fixture(self):
def deps(self):
return inspect.signature(self.fn).parameters

def parents(self) -> List["Fixture"]:
"""
Return the parent fixtures of this fixture, as a list of Fixtures.
"""
return [Fixture(par.default) for par in self.deps().values()]

def teardown(self):
# Suppress because we can't know whether there's more code
# to execute below the yield.
Expand Down Expand Up @@ -135,7 +174,7 @@ def get(
return fixtures.get(fixture_key)


_FIXTURES = []
_DEFINED_FIXTURES = []


def fixture(func=None, *, scope: Optional[Union[Scope, str]] = Scope.Test):
Expand All @@ -155,7 +194,7 @@ def fixture(func=None, *, scope: Optional[Union[Scope, str]] = Scope.Test):
else:
func.ward_meta = WardMeta(is_fixture=True, scope=scope, path=path)

_FIXTURES.append(func)
_DEFINED_FIXTURES.append(Fixture(func))

@wraps(func)
def wrapper(*args, **kwargs):
Expand All @@ -180,3 +219,34 @@ def wrapper(*args, **kwargs):
return wrapper

return decorator_using


def is_fixture(obj: Any) -> bool:
"""
Returns True if and only if the object is a fixture function
(it would be False for a Fixture instance,
but True for the underlying function inside it).
"""
return hasattr(obj, "ward_meta") and obj.ward_meta.is_fixture


_TYPE_FIXTURE_TO_FIXTURES = Mapping[Fixture, Collection[Fixture]]


def fixture_parents_and_children(
fixtures: Iterable[Fixture],
) -> Tuple[_TYPE_FIXTURE_TO_FIXTURES, _TYPE_FIXTURE_TO_FIXTURES]:
"""
Given an iterable of Fixtures, produce two dictionaries:
the first maps each fixture to its parents (the fixtures it depends on);
the second maps each fixture to its children (the fixtures that depend on it).
"""
fixtures_to_parents = {fixture: fixture.parents() for fixture in fixtures}

# not a defaultdict, because we want to have empty entries if no parents when we return
fixtures_to_children = {fixture: [] for fixture in fixtures}
for fixture, parents in fixtures_to_parents.items():
for parent in parents:
fixtures_to_children[parent].append(fixture)

return fixtures_to_parents, fixtures_to_children
50 changes: 33 additions & 17 deletions ward/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@
get_info_for_modules,
get_tests_in_modules,
load_modules,
search_generally,
filter_tests,
filter_fixtures,
)
from ward.config import set_defaults_from_config
from ward.rewrite import rewrite_assertions_in_tests
from ward.suite import Suite
from ward.fixtures import _DEFINED_FIXTURES
from ward.terminal import SimpleTestResultWrite, output_fixtures, get_exit_code

init()
Expand Down Expand Up @@ -53,13 +55,13 @@ def run(ctx: click.Context):
type=click.Path(exists=True),
multiple=True,
is_eager=True,
help="Look for items in PATH.",
help="Look for tests in PATH.",
)
exclude = click.option(
"--exclude",
type=click.STRING,
multiple=True,
help="Paths to ignore while searching for items. Accepts glob patterns.",
help="Paths to ignore while searching for tests. Accepts glob patterns.",
)


Expand All @@ -69,11 +71,11 @@ def run(ctx: click.Context):
@exclude
@click.option(
"--search",
help="Search test names, bodies, descriptions and module names for the search query and only run matching tests.",
help="Search test names, bodies, descriptions and module names for the search query and only keep matching tests.",
)
@click.option(
"--tags",
help="Run tests matching tag expression (e.g. 'unit and not slow').\n",
help="Find tests matching a tag expression (e.g. 'unit and not slow').",
metavar="EXPR",
type=parse_tags,
)
Expand All @@ -95,12 +97,6 @@ def run(ctx: click.Context):
default="standard",
help="Specify the order in which tests should run.",
)
@click.option(
"--exclude",
type=click.STRING,
multiple=True,
help="Paths to ignore while searching for tests. Accepts glob patterns.",
)
@click.option(
"--show-diff-symbols/--hide-diff-symbols",
default=False,
Expand Down Expand Up @@ -146,10 +142,10 @@ def test(
mod_infos = get_info_for_modules(paths, exclude)
modules = list(load_modules(mod_infos))
unfiltered_tests = get_tests_in_modules(modules, capture_output)
tests = list(search_generally(unfiltered_tests, query=search, tag_expr=tags,))
filtered_tests = list(filter_tests(unfiltered_tests, query=search, tag_expr=tags,))

# Rewrite assertions in each test
tests = rewrite_assertions_in_tests(tests)
tests = rewrite_assertions_in_tests(filtered_tests)

time_to_collect = default_timer() - start_run

Expand All @@ -176,6 +172,17 @@ def test(
@config
@path
@exclude
@click.option(
"-f",
"--fixture-path",
help="Only display fixtures defined in or below the given paths.",
multiple=True,
type=Path,
)
@click.option(
"--search",
help="Search fixtures names, bodies, and module names for the search query and only keep matching fixtures.",
)
@click.option(
"--show-scopes/--no-show-scopes",
help="Display each fixture's scope.",
Expand All @@ -187,8 +194,8 @@ def test(
default=False,
)
@click.option(
"--show-direct-dependencies/--no-show-direct-dependencies",
help="Display the fixtures that each fixture depends on directly.",
"--show-dependencies/--no-show-dependencies",
help="Display the fixtures and tests that each fixture depends on and is used by. Only displays direct dependencies; use --show-dependency-trees to show all dependency information.",
default=False,
)
@click.option(
Expand All @@ -208,20 +215,29 @@ def fixtures(
config_path: Optional[Path],
path: Tuple[str],
exclude: Tuple[str],
fixture_path: Tuple[Path],
search: Optional[str],
show_scopes: bool,
show_docstrings: bool,
show_direct_dependencies: bool,
show_dependencies: bool,
show_dependency_trees: bool,
full: bool,
):
"""Show information on fixtures."""
paths = [Path(p) for p in path]
mod_infos = get_info_for_modules(paths, exclude)
modules = list(load_modules(mod_infos))
tests = list(get_tests_in_modules(modules, capture_output=True))

filtered_fixtures = list(
filter_fixtures(_DEFINED_FIXTURES, query=search, paths=fixture_path)
)

output_fixtures(
fixtures=filtered_fixtures,
tests=tests,
show_scopes=show_scopes or full,
show_docstrings=show_docstrings or full,
show_direct_dependencies=show_direct_dependencies or full,
show_dependencies=show_dependencies or full,
show_dependency_trees=show_dependency_trees or full,
)
Loading

0 comments on commit 67ff1bb

Please sign in to comment.