
This is a backwards compatible improvement to the way gabbi works with pytest. Tests are loaded (and yielded in the same way) but they are filtered more correctly, and fixture start and stops are done more correctly. This allows the test count to be accurate and test selection (using -k) to work properly. Fixes #128 Fixes #123 The details of this change are more clear with an understanding of how pytest plugins and hooks work, but the gist is that a plugin has been created and then forcibly installed. The plugin traverse the collected tests to find and filter out the two tests associated with each suite that start and stop fixtures. These are attached as attributes to the first and last selected (if any) tests in the relevant suite. When a test is asked to run: * if it has a 'starter', the start will run * then the test will run itself * it has a prior that has not run, that will be run * as the tests run if a test with a 'stopper' is found, the stopper will be run
138 lines
4.3 KiB
Python
138 lines
4.3 KiB
Python
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
"""A pytest plugin that runs under the covers with gabbi.
|
|
|
|
This is a backwards compatible improvement to the way gabbi can work with
|
|
pytest. Tests are loaded (and yielded in the same way) but they are filtered
|
|
more correctly, and fixture start and stops are done more correctly.
|
|
|
|
This allows the test count to be accurate, which is nice.
|
|
"""
|
|
|
|
import pytest
|
|
|
|
|
|
# Globals storing the test-like functions to be used when starting
|
|
# and stopping a suite.
|
|
STARTS = {}
|
|
STOPS = {}
|
|
|
|
|
|
def get_cleanname(item):
|
|
"""Extract a test name from a pytest Function item."""
|
|
cleanname = item.name[2:]
|
|
cleanname = cleanname[:-2]
|
|
return cleanname
|
|
|
|
|
|
def get_suitename(name):
|
|
"""Extract a test suite from a clean name.
|
|
|
|
This is fragile. It assumes there are no underscores in
|
|
suite names, which is not always true.
|
|
"""
|
|
if name.startswith('start_') or name.startswith('stop_'):
|
|
_, name = name.split('_', 1)
|
|
return name.split('_', 2)[1]
|
|
|
|
|
|
def c_pytest_collection_modifyitems(items, config):
|
|
"""Set the starters and stoppers for a limited collection of tests."""
|
|
latest_suite = None
|
|
latest_item = None
|
|
for item in items:
|
|
cleanname = get_cleanname(item)
|
|
if not cleanname.startswith(
|
|
('stop_driver_', 'start_driver_', 'driver_')):
|
|
continue
|
|
prefix, testname = cleanname.split('_', 1)
|
|
suitename, _ = testname.split('_', 1)
|
|
if prefix == 'start' or prefix == 'stop':
|
|
continue
|
|
if latest_suite != suitename:
|
|
item.starter = STARTS[suitename]
|
|
if latest_item:
|
|
latest_item.stopper = STOPS[
|
|
get_suitename(get_cleanname(latest_item))]
|
|
latest_suite = suitename
|
|
latest_item = item
|
|
# Set the last stopper in the list
|
|
if latest_item:
|
|
latest_item.stopper = STOPS[get_suitename(get_cleanname(latest_item))]
|
|
|
|
|
|
def a_pytest_collection_modifyitems(items, config):
|
|
"""Traverse collected tests to save START and STOPS.
|
|
|
|
Remove those START and STOPS from the tests to run.
|
|
"""
|
|
remaining = []
|
|
deselected = []
|
|
for item in items:
|
|
cleanname = get_cleanname(item)
|
|
if not cleanname.startswith(
|
|
('stop_driver_', 'start_driver_', 'driver_')):
|
|
remaining.append(item)
|
|
continue
|
|
suitename = get_suitename(cleanname)
|
|
if cleanname.startswith('start_'):
|
|
STARTS[suitename] = item
|
|
deselected.append(item)
|
|
elif cleanname.startswith('stop_'):
|
|
STOPS[suitename] = item
|
|
deselected.append(item)
|
|
else:
|
|
remaining.append(item)
|
|
|
|
if deselected:
|
|
items[:] = remaining
|
|
|
|
|
|
@pytest.hookimpl(hookwrapper=True)
|
|
def pytest_collection_modifyitems(items, config):
|
|
"""Hook for processing collected tests.
|
|
|
|
Discover start and stops, then use the default hook
|
|
for filter for keywords and markers, then attach
|
|
starter and stopper to the remaining tests.
|
|
"""
|
|
a_pytest_collection_modifyitems(items, config)
|
|
yield
|
|
c_pytest_collection_modifyitems(items, config)
|
|
|
|
|
|
def pytest_runtest_setup(item):
|
|
"""Run a starter if a test has one.
|
|
|
|
This is done before run, so it means that a single test will
|
|
run its priors after running this.
|
|
"""
|
|
if hasattr(item, 'starter'):
|
|
try:
|
|
# Python 2
|
|
item.starter.function(item.starter.obj.__self__, item._args[0])
|
|
except TypeError:
|
|
# Python 3
|
|
item.starter.function(item._args[0])
|
|
|
|
|
|
def pytest_runtest_teardown(item, nextitem):
|
|
"""Run a stopper if a test has one."""
|
|
if hasattr(item, 'stopper'):
|
|
try:
|
|
# Python 2
|
|
item.stopper.function(item.stopper.obj.__self__)
|
|
except TypeError:
|
|
# Python 3
|
|
item.stopper.function()
|