2014-12-15 13:04:08 +00:00
|
|
|
#
|
|
|
|
# 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.
|
|
|
|
|
|
|
|
"""Generate HTTP tests from YAML files
|
|
|
|
|
|
|
|
Each HTTP request is its own TestCase and can be requested to be run in
|
2015-04-08 00:00:03 +01:00
|
|
|
isolation from other tests. If it is a member of a sequence of requests,
|
2014-12-15 13:04:08 +00:00
|
|
|
prior requests will be run.
|
|
|
|
|
|
|
|
A sequence is represented by an ordered list in a single YAML file.
|
|
|
|
|
|
|
|
Each sequence becomes a TestSuite.
|
|
|
|
|
|
|
|
An entire directory of YAML files is a TestSuite of TestSuites.
|
|
|
|
"""
|
2014-10-29 16:05:26 +00:00
|
|
|
|
2015-04-20 18:39:54 +01:00
|
|
|
import copy
|
2014-10-29 16:05:26 +00:00
|
|
|
import glob
|
2014-12-27 18:50:05 +00:00
|
|
|
import inspect
|
2014-10-29 16:05:26 +00:00
|
|
|
import os
|
2014-12-15 13:04:08 +00:00
|
|
|
from unittest import suite
|
2014-12-15 21:55:14 +00:00
|
|
|
import uuid
|
2014-10-29 16:05:26 +00:00
|
|
|
|
2015-04-20 18:39:54 +01:00
|
|
|
import six
|
2014-10-30 15:11:47 +00:00
|
|
|
import yaml
|
2014-10-29 16:05:26 +00:00
|
|
|
|
2015-01-26 19:49:42 +00:00
|
|
|
from gabbi import case
|
2015-03-27 16:33:53 +00:00
|
|
|
from gabbi import handlers
|
2015-08-09 18:13:05 +01:00
|
|
|
from gabbi import httpclient
|
2015-01-26 19:49:42 +00:00
|
|
|
from gabbi import suite as gabbi_suite
|
2014-12-27 23:39:39 +00:00
|
|
|
|
2015-03-27 16:33:53 +00:00
|
|
|
RESPONSE_HANDLERS = [
|
2015-07-23 12:13:24 +01:00
|
|
|
handlers.HeadersResponseHandler,
|
2015-03-27 16:33:53 +00:00
|
|
|
handlers.StringResponseHandler,
|
|
|
|
handlers.JSONResponseHandler,
|
|
|
|
]
|
|
|
|
|
2014-10-30 15:11:47 +00:00
|
|
|
|
2015-07-22 17:49:53 +01:00
|
|
|
class GabbiFormatError(ValueError):
|
|
|
|
"""An exception to encapsulate poorly formed test data."""
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
2015-10-08 19:00:21 +01:00
|
|
|
class TestMaker(object):
|
|
|
|
"""A class for encapsulating test invariants.
|
|
|
|
|
|
|
|
All of the tests in a single gabbi file have invariants which are
|
|
|
|
provided when creating each HTTPTestCase. It is not useful
|
|
|
|
to pass these around when making each test case. So they are
|
|
|
|
wrapped in this class which then has make_one_test called multiple
|
|
|
|
times to generate all the tests in the suite.
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(self, test_base_name, base_test_data, test_directory,
|
2015-10-10 18:00:46 +01:00
|
|
|
fixture_classes, loader, host, port, intercept, prefix):
|
2015-10-08 19:00:21 +01:00
|
|
|
self.test_base_name = test_base_name
|
|
|
|
self.base_test_data = base_test_data
|
|
|
|
self.base_test_key_set = set(base_test_data.keys())
|
|
|
|
self.test_directory = test_directory
|
|
|
|
self.fixture_classes = fixture_classes
|
2015-10-10 18:00:46 +01:00
|
|
|
self.host = host
|
|
|
|
self.port = port
|
2015-10-08 19:00:21 +01:00
|
|
|
self.loader = loader
|
2015-10-10 18:00:46 +01:00
|
|
|
self.intercept = intercept
|
|
|
|
self.prefix = prefix
|
2015-10-08 19:00:21 +01:00
|
|
|
|
2015-10-10 18:00:46 +01:00
|
|
|
def make_one_test(self, test_datum, prior_test):
|
2015-10-08 19:00:21 +01:00
|
|
|
"""Create one single HTTPTestCase.
|
|
|
|
|
|
|
|
The returned HTTPTestCase is added to the TestSuite currently
|
|
|
|
being built (one per YAML file).
|
|
|
|
"""
|
|
|
|
test = copy.deepcopy(self.base_test_data)
|
|
|
|
try:
|
|
|
|
test_update(test, test_datum)
|
|
|
|
except KeyError as exc:
|
|
|
|
raise GabbiFormatError('invalid key in test: %s' % exc)
|
|
|
|
except AttributeError as exc:
|
|
|
|
if not isinstance(test_datum, dict):
|
|
|
|
raise GabbiFormatError(
|
|
|
|
'test chunk is not a dict at "%s"' % test_datum)
|
|
|
|
else:
|
2015-10-10 18:00:46 +01:00
|
|
|
# NOTE(cdent): Not clear this can happen but just in case.
|
2015-10-08 19:00:21 +01:00
|
|
|
raise GabbiFormatError(
|
|
|
|
'malformed test chunk "%s": %s' % (test_datum, exc))
|
|
|
|
|
2015-10-10 18:00:46 +01:00
|
|
|
self._set_test_name(test)
|
|
|
|
self._set_test_method_and_url(test)
|
|
|
|
self._validate_keys(test)
|
|
|
|
|
|
|
|
http_class = httpclient.get_http(verbose=test['verbose'],
|
|
|
|
caption=test['name'])
|
|
|
|
|
|
|
|
# Use metaclasses to build a class of the necessary type
|
|
|
|
# and name with relevant arguments.
|
|
|
|
klass = TestBuilder(test['name'], (case.HTTPTestCase,),
|
|
|
|
{'test_data': test,
|
|
|
|
'test_directory': self.test_directory,
|
|
|
|
'fixtures': self.fixture_classes,
|
|
|
|
'http': http_class,
|
|
|
|
'host': self.host,
|
|
|
|
'intercept': self.intercept,
|
|
|
|
'port': self.port,
|
|
|
|
'prefix': self.prefix,
|
|
|
|
'prior': prior_test})
|
|
|
|
|
|
|
|
tests = self.loader.loadTestsFromTestCase(klass)
|
|
|
|
# Return the first (and only) test in the klass.
|
|
|
|
return tests._tests[0]
|
|
|
|
|
|
|
|
def _set_test_name(self, test):
|
|
|
|
"""Set the name of the test
|
|
|
|
|
|
|
|
The original name is lowercased and spaces are replaces with '_'.
|
|
|
|
The result is appended to the test_base_name, which is based on the
|
|
|
|
name of the input data file.
|
|
|
|
"""
|
2015-10-08 19:00:21 +01:00
|
|
|
if not test['name']:
|
|
|
|
raise GabbiFormatError('Test name missing in a test in %s.'
|
|
|
|
% self.test_base_name)
|
2015-10-10 18:00:46 +01:00
|
|
|
test['name'] = '%s_%s' % (self.test_base_name,
|
|
|
|
test['name'].lower().replace(' ', '_'))
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def _set_test_method_and_url(test):
|
|
|
|
"""Extract the base URL and method for this test.
|
2015-10-08 19:00:21 +01:00
|
|
|
|
2015-10-10 18:00:46 +01:00
|
|
|
If there is an upper case key in the test, that is used as the
|
|
|
|
method and the value is used as the URL. If there is more than
|
|
|
|
one uppercase that is a GabbiFormatError.
|
|
|
|
|
|
|
|
If there is no upper case key then 'url' must be present.
|
|
|
|
"""
|
2015-10-08 19:00:21 +01:00
|
|
|
method_key = None
|
|
|
|
for key, val in six.iteritems(test):
|
|
|
|
if _is_method_shortcut(key):
|
|
|
|
if method_key:
|
|
|
|
raise GabbiFormatError(
|
|
|
|
'duplicate method/URL directive in "%s"' %
|
2015-10-10 18:00:46 +01:00
|
|
|
test['name'])
|
2015-10-08 19:00:21 +01:00
|
|
|
|
|
|
|
test['method'] = key
|
|
|
|
test['url'] = val
|
|
|
|
method_key = key
|
|
|
|
if method_key:
|
|
|
|
del test[method_key]
|
|
|
|
|
|
|
|
if not test['url']:
|
|
|
|
raise GabbiFormatError('Test url missing in test %s.'
|
2015-10-10 18:00:46 +01:00
|
|
|
% test['name'])
|
|
|
|
|
|
|
|
def _validate_keys(self, test):
|
|
|
|
"""Check for invalid keys.
|
2015-10-08 19:00:21 +01:00
|
|
|
|
2015-10-10 18:00:46 +01:00
|
|
|
If there are any, raise a GabbiFormatError.
|
|
|
|
"""
|
2015-10-08 19:00:21 +01:00
|
|
|
test_key_set = set(test.keys())
|
|
|
|
if test_key_set != self.base_test_key_set:
|
|
|
|
raise GabbiFormatError(
|
|
|
|
'Invalid test keys used in test %s: %s'
|
2015-10-10 18:00:46 +01:00
|
|
|
% (test['name'],
|
2015-10-08 19:00:21 +01:00
|
|
|
', '.join(list(test_key_set - self.base_test_key_set))))
|
|
|
|
|
|
|
|
|
2014-12-15 13:33:00 +00:00
|
|
|
class TestBuilder(type):
|
2015-02-10 22:51:45 +00:00
|
|
|
"""Metaclass to munge a dynamically created test."""
|
2014-12-15 13:33:00 +00:00
|
|
|
|
|
|
|
required_attributes = {'has_run': False}
|
|
|
|
|
|
|
|
def __new__(mcs, name, bases, attributes):
|
|
|
|
attributes.update(mcs.required_attributes)
|
|
|
|
return type.__new__(mcs, name, bases, attributes)
|
|
|
|
|
|
|
|
|
2014-12-27 18:50:05 +00:00
|
|
|
def build_tests(path, loader, host=None, port=8001, intercept=None,
|
2015-03-28 18:07:12 +00:00
|
|
|
test_loader_name=None, fixture_module=None,
|
2015-10-08 17:54:00 +01:00
|
|
|
response_handlers=None, prefix=''):
|
2014-12-15 13:04:08 +00:00
|
|
|
"""Read YAML files from a directory to create tests.
|
|
|
|
|
|
|
|
Each YAML file represents an ordered sequence of HTTP requests.
|
2015-06-17 12:04:25 +01:00
|
|
|
|
|
|
|
:param path: The directory where yaml files are located.
|
|
|
|
:param loader: The TestLoader.
|
2015-06-17 12:16:24 +01:00
|
|
|
:param host: The host to test against. Do not use with ``intercept``.
|
2015-06-17 12:04:25 +01:00
|
|
|
:param port: The port to test against. Used with ``host``.
|
|
|
|
:param intercept: WSGI app factory for wsgi-intercept.
|
|
|
|
:param test_loader_name: Base name for test classes. Rarely used.
|
|
|
|
:param fixture_module: Python module containing fixture classes.
|
|
|
|
:param response_handers: ResponseHandler classes.
|
|
|
|
:type response_handlers: List of ResponseHandler classes.
|
|
|
|
:param prefix: A URL prefix for all URLs that are not fully qualified.
|
2015-06-17 12:16:24 +01:00
|
|
|
:rtype: TestSuite containing multiple TestSuites (one for each YAML file).
|
2014-12-15 13:04:08 +00:00
|
|
|
"""
|
2015-03-10 16:04:54 +00:00
|
|
|
|
2015-05-26 23:04:37 +01:00
|
|
|
if not (bool(host) ^ bool(intercept)):
|
|
|
|
raise AssertionError('must specify exactly one of host or intercept')
|
2015-03-10 16:04:54 +00:00
|
|
|
|
2015-03-28 18:07:12 +00:00
|
|
|
response_handlers = response_handlers or []
|
2014-12-12 16:51:27 +00:00
|
|
|
top_suite = suite.TestSuite()
|
2014-12-15 21:55:14 +00:00
|
|
|
|
2015-01-14 14:31:10 +00:00
|
|
|
if test_loader_name is None:
|
|
|
|
test_loader_name = inspect.stack()[1]
|
|
|
|
test_loader_name = os.path.splitext(os.path.basename(
|
|
|
|
test_loader_name[1]))[0]
|
2014-12-15 21:55:14 +00:00
|
|
|
|
2015-01-14 14:31:10 +00:00
|
|
|
yaml_file_glob = '%s/*.yaml' % path
|
2014-12-12 14:44:33 +00:00
|
|
|
|
2015-03-28 18:07:12 +00:00
|
|
|
# Initialize the extensions for response handling.
|
|
|
|
for handler in RESPONSE_HANDLERS + response_handlers:
|
2015-03-27 16:33:53 +00:00
|
|
|
handler(case.HTTPTestCase)
|
|
|
|
|
2015-01-14 14:31:10 +00:00
|
|
|
# Return an empty suite if we have no host to access, either via
|
|
|
|
# a real host or an intercept
|
2015-03-10 16:04:54 +00:00
|
|
|
for test_file in glob.iglob(yaml_file_glob):
|
|
|
|
if intercept:
|
|
|
|
host = str(uuid.uuid4())
|
|
|
|
test_yaml = load_yaml(test_file)
|
2015-10-10 18:00:46 +01:00
|
|
|
test_base_name = '%s_%s' % (test_loader_name, os.path.splitext(
|
|
|
|
os.path.basename(test_file))[0])
|
|
|
|
file_suite = test_suite_from_yaml(loader, test_base_name, test_yaml,
|
2015-03-10 16:04:54 +00:00
|
|
|
path, host, port, fixture_module,
|
2015-06-17 12:04:25 +01:00
|
|
|
intercept, prefix)
|
2015-03-10 16:04:54 +00:00
|
|
|
top_suite.addTest(file_suite)
|
2014-12-12 14:44:33 +00:00
|
|
|
return top_suite
|
2014-12-15 13:33:00 +00:00
|
|
|
|
|
|
|
|
|
|
|
def load_yaml(yaml_file):
|
2014-12-15 21:55:14 +00:00
|
|
|
"""Read and parse any YAML file. Let exceptions flow where they may."""
|
2014-12-15 13:33:00 +00:00
|
|
|
with open(yaml_file) as source:
|
|
|
|
return yaml.safe_load(source.read())
|
2015-01-14 14:31:10 +00:00
|
|
|
|
|
|
|
|
2015-04-20 18:39:54 +01:00
|
|
|
def test_update(orig_dict, new_dict):
|
|
|
|
"""Modify test in place to update with new data."""
|
|
|
|
for key, val in six.iteritems(new_dict):
|
|
|
|
if key == 'data':
|
|
|
|
orig_dict[key] = val
|
|
|
|
elif isinstance(val, dict):
|
|
|
|
orig_dict[key].update(val)
|
|
|
|
elif isinstance(val, list):
|
|
|
|
orig_dict[key] = orig_dict.get(key, []) + val
|
|
|
|
else:
|
|
|
|
orig_dict[key] = val
|
|
|
|
|
|
|
|
|
2015-01-14 15:37:48 +00:00
|
|
|
def test_suite_from_yaml(loader, test_base_name, test_yaml, test_directory,
|
2015-10-08 17:54:00 +01:00
|
|
|
host, port, fixture_module, intercept, prefix=''):
|
2015-10-08 19:00:21 +01:00
|
|
|
"""Generate a TestSuite from YAML-sourced data."""
|
2015-01-26 19:49:42 +00:00
|
|
|
file_suite = gabbi_suite.GabbiSuite()
|
2015-10-10 18:00:46 +01:00
|
|
|
|
2015-07-22 17:49:53 +01:00
|
|
|
try:
|
|
|
|
test_data = test_yaml['tests']
|
|
|
|
except KeyError:
|
|
|
|
raise GabbiFormatError(
|
|
|
|
'malformed test file, "tests" key required')
|
|
|
|
except TypeError:
|
|
|
|
# Swallow this exception as displaying it does not shine a
|
|
|
|
# light on the path to fix it.
|
|
|
|
raise GabbiFormatError('malformed test file, invalid format')
|
|
|
|
|
2015-01-14 14:31:10 +00:00
|
|
|
fixtures = test_yaml.get('fixtures', None)
|
|
|
|
|
2015-10-08 19:00:21 +01:00
|
|
|
# Set defaults from BASE_TEST then update those defaults
|
2015-01-14 14:31:10 +00:00
|
|
|
# with any defaults set in the YAML file.
|
2015-04-20 18:39:54 +01:00
|
|
|
base_test_data = copy.deepcopy(case.HTTPTestCase.base_test)
|
2015-09-11 17:00:22 +01:00
|
|
|
defaults = _validate_defaults(test_yaml.get('defaults', {}))
|
2015-04-20 18:39:54 +01:00
|
|
|
test_update(base_test_data, defaults)
|
2015-01-14 14:31:10 +00:00
|
|
|
|
2015-10-08 19:00:21 +01:00
|
|
|
# Establish any fixture classes used in this file.
|
2015-01-14 14:31:10 +00:00
|
|
|
fixture_classes = []
|
|
|
|
if fixtures and fixture_module:
|
|
|
|
for fixture_class in fixtures:
|
|
|
|
fixture_classes.append(getattr(fixture_module, fixture_class))
|
|
|
|
|
2015-10-08 19:00:21 +01:00
|
|
|
test_maker = TestMaker(test_base_name, base_test_data, test_directory,
|
2015-10-10 18:00:46 +01:00
|
|
|
fixture_classes, loader, host, port, intercept,
|
|
|
|
prefix)
|
2015-01-14 14:31:10 +00:00
|
|
|
prior_test = None
|
|
|
|
for test_datum in test_data:
|
2015-10-10 18:00:46 +01:00
|
|
|
this_test = test_maker.make_one_test(test_datum, prior_test)
|
2015-01-14 14:31:10 +00:00
|
|
|
file_suite.addTest(this_test)
|
|
|
|
prior_test = this_test
|
|
|
|
|
|
|
|
return file_suite
|
2015-09-11 17:00:22 +01:00
|
|
|
|
|
|
|
|
2015-09-12 11:57:59 +02:00
|
|
|
def _validate_defaults(defaults):
|
2015-09-14 19:23:47 +02:00
|
|
|
"""Ensure default test settings are acceptable
|
2015-09-12 11:57:44 +02:00
|
|
|
|
|
|
|
Raises GabbiFormatError for invalid settings.
|
|
|
|
"""
|
2015-09-12 12:00:34 +02:00
|
|
|
if any(_is_method_shortcut(key) for key in defaults):
|
2015-09-11 17:00:22 +01:00
|
|
|
raise GabbiFormatError(
|
|
|
|
'"METHOD: url" pairs not allowed in defaults')
|
2015-09-12 11:57:59 +02:00
|
|
|
return defaults
|
2015-09-11 18:22:27 +02:00
|
|
|
|
|
|
|
|
|
|
|
def _is_method_shortcut(key):
|
|
|
|
return key.isupper()
|