Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added support for custom response handlers to gabbi-run #95

Merged
merged 11 commits into from Oct 1, 2015
2 changes: 1 addition & 1 deletion docs/source/example.rst
Expand Up @@ -24,7 +24,7 @@ bit like this:
:language: python

For details on the arguments available when building tests see
:func:`~gabbi.driver.build_tests`.
:meth:`~gabbi.driver.build_tests`.

Run Test Loader
~~~~~~~~~~~~~~~
Expand Down
6 changes: 5 additions & 1 deletion docs/source/handlers.rst
Expand Up @@ -18,12 +18,16 @@ whether the test passes or fails.
While the default handlers (as described in :doc:`format`) are sufficient for
most cases, it is possible to register additional custom handlers by passing a
subclass of :class:`~gabbi.handlers.ResponseHandler` to
:func:`~gabbi.driver.build_tests`::
:meth:`~gabbi.driver.build_tests`::

driver.build_tests(test_dir, loader, host=None,
intercept=simple_wsgi.SimpleWsgi,
response_handlers=[MyResponseHandler])

With ``gabbi-run``, custom handlers can be loaded via the
``--response-handler`` option -- see
:meth:`~gabbi.runner.load_response_handlers` for details.

A subclass needs to define at least three things:

* ``test_key_suffix``: This, along with the prefix ``response_``, forms
Expand Down
50 changes: 44 additions & 6 deletions gabbi/runner.py
Expand Up @@ -16,6 +16,8 @@
import unittest
import yaml

from importlib import import_module

from six.moves.urllib import parse as urlparse

from gabbi import case
Expand Down Expand Up @@ -75,12 +77,24 @@ def run():
)
parser.add_argument(
'-x', '--failfast',
help='Exit on first failure',
action='store_true'
action='store_true',
help='Exit on first failure'
)
parser.add_argument(
'-r', '--response-handler',
nargs='?', default=None,
dest='response_handlers',
action='append',
help='Custom response handler. Should be an import path of the '
'form package.module or package.module:class.'
)

args = parser.parse_args()

custom_response_handlers = []
for import_path in (args.response_handlers or []):
for handler in load_response_handlers(import_path):
custom_response_handlers.append(handler)

split_url = urlparse.urlsplit(args.target)
if split_url.scheme:
target = split_url.netloc
Expand All @@ -95,19 +109,43 @@ def run():
host = target
port = None

loader = unittest.defaultTestLoader

# Initialize the extensions for response handling.
for handler in driver.RESPONSE_HANDLERS:
for handler in (driver.RESPONSE_HANDLERS + custom_response_handlers):
handler(case.HTTPTestCase)

data = yaml.safe_load(sys.stdin.read())
loader = unittest.defaultTestLoader
suite = driver.test_suite_from_yaml(loader, 'input', data, '.',
host, port, None, None,
prefix=prefix)
result = ConciseTestRunner(verbosity=2, failfast=args.failfast).run(suite)
sys.exit(not result.wasSuccessful())


def load_response_handlers(import_path):
"""Load and return custom response handlers from the given Python package
or module.

The import path references either a specific response handler class
(package.module:class) or a module that contains one or more response
handler classes (package.module).

For the latter, the module is expected to contain a gabbi_response_handlers
object, which is either a list of response handler classes or a function
returning such a list.
"""
if ":" in import_path: # package.module:class
module_name, handler_name = import_path.rsplit(":", 1)
module = import_module(module_name)
handler = getattr(module, handler_name)
handlers = [handler]
else: # package.module shorthand, expecting gabbi_response_handlers
module = import_module(import_path)
handlers = module.gabbi_response_handlers
if callable(handlers):
handlers = handlers()
return handlers


if __name__ == '__main__':
run()
16 changes: 15 additions & 1 deletion gabbi/simple_wsgi.py
Expand Up @@ -78,7 +78,21 @@ def __call__(self, environ, start_response):
query_data = body_data
headers.append(('Location', full_request_url))

if path_info.startswith('/poller'):
if path_info == '/presenter':
start_response('200 OK', [('Content-Type', 'text/html')])
return [b"""<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Hello World</title>
</head>
<body>
<h1>Hello World</h1>
<p>lorem ipsum dolor sit amet</p>
</body>
</html>
"""]
elif path_info.startswith('/poller'):
if CURRENT_POLL == 0:
CURRENT_POLL = int(query_data.get('count', [5])[0])
start_response('400 Bad Reqest', [])
Expand Down
27 changes: 27 additions & 0 deletions gabbi/tests/custom_response_handler.py
@@ -0,0 +1,27 @@
#
# 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.

from gabbi import handlers


def gabbi_response_handlers():
return [CustomResponseHandler]


class CustomResponseHandler(handlers.ResponseHandler):

test_key_suffix = 'custom'
test_key_value = []

def action(self, test, item, value=None):
test.assertTrue(item in test.output)
190 changes: 190 additions & 0 deletions gabbi/tests/test_runner.py
@@ -0,0 +1,190 @@
#
# 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.
"""Test that the CLI works as expected
"""

import sys
import unittest

from six import StringIO

from gabbi import driver
from gabbi.fixture import InterceptFixture
from gabbi import handlers
from gabbi import runner
from gabbi.simple_wsgi import SimpleWsgi


class RunnerTest(unittest.TestCase):

def setUp(self):
super(RunnerTest, self).setUp()

# NB: random host ensures that we're not accidentally connecting to an
# actual server
host, port = ('eefc1156-b364-4d2f-a652-ff6aec23c7f6', 8000)
self.server = lambda: InterceptFixture(host, port, SimpleWsgi, '')

self._stdin = sys.stdin

self._stdout = sys.stdout
sys.stdout = StringIO() # swallow output to avoid confusion

self._stderr = sys.stderr
sys.stderr = StringIO() # swallow output to avoid confusion

self._argv = sys.argv
sys.argv = ['gabbi-run', '%s:%s' % (host, port)]

def tearDown(self):
sys.stdin = self._stdin
sys.stdout = self._stdout
sys.stderr = self._stderr
sys.argv = self._argv

def test_custom_response_handler(self):
sys.stdin = StringIO("""
tests:
- name: unknown response handler
GET: /
response_html: ...
""")
with self.assertRaises(driver.GabbiFormatError):
runner.run()

sys.argv.insert(1, "--response-handler")
sys.argv.insert(2, "gabbi.tests.test_runner:HTMLResponseHandler")

sys.stdin = StringIO("""
tests:
- name: custom response handler
GET: /presenter
response_html:
h1: Hello World
p: lorem ipsum dolor sit amet
""")
with self.server():
try:
runner.run()
except SystemExit as err:
self.assertSuccess(err)

sys.stdin = StringIO("""
tests:
- name: custom response handler failure
GET: /presenter
response_html:
h1: lipsum
""")
with self.server():
try:
runner.run()
except SystemExit as err:
self.assertFailure(err)

sys.argv.insert(3, "-r")
sys.argv.insert(4, "gabbi.test_intercept:TestResponseHandler")

sys.stdin = StringIO("""
tests:
- name: additional custom response handler
GET: /presenter
response_html:
h1: Hello World
response_test:
- COWAnother line
""")
with self.server():
try:
runner.run()
except SystemExit as err:
self.assertSuccess(err)

sys.argv.insert(5, "-r")
sys.argv.insert(6, "gabbi.tests.custom_response_handler")

sys.stdin = StringIO("""
tests:
- name: custom response handler shorthand
GET: /presenter
response_custom:
- Hello World
- lorem ipsum dolor sit amet
""")
with self.server():
try:
runner.run()
except SystemExit as err:
self.assertSuccess(err)

def test_exit_code(self):
sys.stdin = StringIO()
with self.assertRaises(driver.GabbiFormatError):
runner.run()

sys.stdin = StringIO("""
tests:
- name: expected failure
GET: /
status: 666
""")
try:
runner.run()
except SystemExit as err:
self.assertFailure(err)

sys.stdin = StringIO("""
tests:
- name: expected success
GET: /
status: 200
""")
with self.server():
try:
runner.run()
except SystemExit as err:
self.assertSuccess(err)

def assertSuccess(self, exitError):
errors = exitError.args[0]
if errors:
self._dump_captured()
self.assertEqual(errors, False)

def assertFailure(self, exitError):
errors = exitError.args[0]
if not errors:
self._dump_captured()
self.assertEqual(errors, True)

def _dump_captured(self):
self._stderr.write('\n==> captured STDOUT <==\n')
sys.stdout.flush()
sys.stdout.seek(0)
self._stderr.write(sys.stdout.read())

self._stderr.write('\n==> captured STDERR <==\n')
sys.stderr.flush()
sys.stderr.seek(0)
self._stderr.write(sys.stderr.read())


class HTMLResponseHandler(handlers.ResponseHandler):

test_key_suffix = 'html'
test_key_value = {}

def action(self, test, item, value):
doc = test.output
html = '<{tag}>{content}</{tag}>'.format(tag=item, content=value)
test.assertTrue(html in doc, "no elements matching '%s'" % html)