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

Feature/new hooks #177

Closed
wants to merge 19 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions tavern/_plugins/mqtt/response.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import logging
import json
import logging
import time

from tavern.util import exceptions
from tavern.response.base import BaseResponse
from tavern.util import exceptions
from tavern.util.dict_util import check_keys_match_recursive
from tavern.util.loader import ANYTHING
from .util import get_paho_mqtt_response_information

try:
LoadException = json.decoder.JSONDecodeError
Expand All @@ -17,9 +18,7 @@


class MQTTResponse(BaseResponse):
def __init__(self, client, name, expected, test_block_config):
# pylint: disable=unused-argument

def __init__(self, client, name, expected, test_block_config): # pylint: disable=unused-argument
super(MQTTResponse, self).__init__()

self.name = name
Expand Down Expand Up @@ -82,6 +81,12 @@ def _await_response(self):

msg.payload = msg.payload.decode("utf8")

self.test_block_config["tavern_internal"][
"pytest_hook_caller"
].pytest_tavern_log_response(
response=get_paho_mqtt_response_information(msg)
)

if json_payload:
try:
msg.payload = json.loads(msg.payload)
Expand Down
15 changes: 15 additions & 0 deletions tavern/_plugins/mqtt/util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
def get_paho_mqtt_response_information(message):
"""Get parsed MQTT message information

Args:
message (paho.mqtt.MQTTMessage): paho message object

Returns:
dict: dictionary with useful message information to log
"""
info = {}

info["topic"] = message.topic
info["payload"] = message.payload

return info
60 changes: 33 additions & 27 deletions tavern/_plugins/rest/response.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import json
import logging
import traceback

try:
from urllib.parse import urlparse, parse_qs
except ImportError:
from urlparse import urlparse, parse_qs # type: ignore
from builtins import str as ustr

from requests.status_codes import _codes

Expand All @@ -14,6 +10,7 @@
from tavern.util import exceptions
from tavern.util.exceptions import TestFailError
from tavern.response.base import BaseResponse, indent_err_text
from .util import get_redirect_query_params, get_requests_response_information

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -53,43 +50,48 @@ def __str__(self):

def _verbose_log_response(self, response):
"""Verbosely log the response object, with query params etc."""
# pylint: disable=no-self-use

logger.info("Response: '%s'", response)

# Reuse this. The hooks will get called, but also print it out here for
# usability/backwards compatability.
response_info = get_requests_response_information(response)

def log_dict_block(block, name):
if block:
to_log = name + ":"

def enc(v):
if isinstance(v, ustr):
return v.encode("utf8")
else:
return v

if isinstance(block, list):
for v in block:
to_log += "\n - {}".format(v)
to_log += "\n - {}".format(enc(v))
elif isinstance(block, dict):
for k, v in block.items():
to_log += "\n {}: {}".format(k, v)
to_log += "\n {}: {}".format(enc(k), enc(v))
else:
to_log += "\n {}".format(block)
logger.debug(to_log)

log_dict_block(response.headers, "Headers")

try:
log_dict_block(response.json(), "Body")
except ValueError:
pass

redirect_query_params = self._get_redirect_query_params(response)
if redirect_query_params:
parsed_url = urlparse(response.headers["location"])
to_path = "{0}://{1}{2}".format(*parsed_url)
logger.debug("Redirect location: %s", to_path)
log_dict_block(redirect_query_params, "Redirect URL query parameters")
log_dict_block(response_info["headers"], "Headers")
log_dict_block(response_info["body"], "Body")
if response_info.get("redirect_query_location"):
log_dict_block(
response_info["redirect_query_location"], "Redirect location"
)

def _get_redirect_query_params(self, response):
"""If there was a redirect header, get any query parameters from it
def _get_redirect_query_params_with_err_check(self, response):
"""Call get_redirect_query_params, but also trigger an error if we
expected a redirection
"""

try:
redirect_url = response.headers["location"]
response.headers["location"]
except KeyError as e:
if "redirect_query_params" in self.expected.get("save", {}):
self._adderr(
Expand All @@ -99,9 +101,7 @@ def _get_redirect_query_params(self, response):
)
redirect_query_params = {}
else:
parsed = urlparse(redirect_url)
qp = parsed.query
redirect_query_params = {i: j[0] for i, j in parse_qs(qp).items()}
redirect_query_params = get_redirect_query_params(response)

return redirect_query_params

Expand Down Expand Up @@ -149,6 +149,12 @@ def verify(self, response):
"""
self._verbose_log_response(response)

self.test_block_config["tavern_internal"][
"pytest_hook_caller"
].pytest_tavern_log_response(
response=get_requests_response_information(response)
)

self.response = response
self.status_code = response.status_code

Expand All @@ -173,7 +179,7 @@ def verify(self, response):
# Get any keys to save
saved = {}

redirect_query_params = self._get_redirect_query_params(response)
redirect_query_params = self._get_redirect_query_params_with_err_check(response)

saved.update(self._save_value("body", body))
saved.update(self._save_value("headers", response.headers))
Expand Down
50 changes: 50 additions & 0 deletions tavern/_plugins/rest/util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
try:
from urllib.parse import urlparse, parse_qs
except ImportError:
from urlparse import urlparse, parse_qs


def get_redirect_query_params(response):
"""If there was a redirect header, get any query parameters from it
"""

try:
redirect_url = response.headers["location"]
except KeyError:
redirect_query_params = {}
else:
parsed = urlparse(redirect_url)
qp = parsed.query
redirect_query_params = {i: j[0] for i, j in parse_qs(qp).items()}

return redirect_query_params


def get_requests_response_information(response):
"""Get response parameters to print/log

Args:
response (requests.Response): response object from requests

Returns:
dict: dict containing body, headers, and redirect params
"""
info = {}
info["headers"] = response.headers

try:
info["body"] = response.json()
except ValueError:
info["body"] = None

redirect_query_params = get_redirect_query_params(response)
if redirect_query_params:
parsed_url = urlparse(response.headers["location"])
to_path = "{0}://{1}{2}".format(*parsed_url)
info["redirect_query_params"] = redirect_query_params
info["redirect_location"] = to_path
else:
info["redirect_query_params"] = None
info["redirect_location"] = None

return info
14 changes: 12 additions & 2 deletions tavern/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ def run_stage(sessions, stage, tavern_box, test_block_config):
"""Run one stage from the test

Args:
sessions (dict): Dictionary of relevant 'session' objects used for this test
sessions (dict): mapping of relevant 'session' objects used for this test
stage (dict): specification of stage to be run
tavern_box (box.Box): Box object containing format variables to be used
in test
Expand All @@ -234,9 +234,19 @@ def run_stage(sessions, stage, tavern_box, test_block_config):
delay(stage, "before", test_block_config["variables"])

logger.info("Running stage : %s", name)
response = r.run()

hook_caller = test_block_config["tavern_internal"]["pytest_hook_caller"]

# These are run directly before/after to allow timing
hook_caller.pytest_tavern_before_request(stage=stage)
try:
response = r.run()
finally:
# Called even if the test fails
hook_caller.pytest_tavern_after_request(stage=stage)

verifiers = get_verifiers(stage, test_block_config, sessions, expected)

for v in verifiers:
saved = v.verify(response)
test_block_config["variables"].update(saved)
Expand Down
3 changes: 3 additions & 0 deletions tavern/request/base.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from abc import abstractmethod
import logging

logger = logging.getLogger(__name__)


class BaseRequest(object):
Expand Down
9 changes: 9 additions & 0 deletions tavern/response/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,12 @@ def _check_for_validate_functions(self, payload):
if isinstance(payload, dict):
if "$ext" in payload:
self.validate_function = get_wrapped_response_function(payload["$ext"])

def get_parsed_response(self, response): # pylint: disable=unused-argument
"""Get a dict/list representation of the response

Should be overridden by plugins, but it's not _required_ because it
might not be used
"""
logger.warning("get_parsed_response not overridden for %s - no useful information can be logged about this response", type(self))
return {}
7 changes: 7 additions & 0 deletions tavern/testutils/pytesthook/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,10 @@ def pytest_collect_file(parent, path):
return YamlFile(path, parent)

return None


def pytest_addhooks(pluginmanager):
"""Add our custom tavern hooks"""
from . import newhooks

pluginmanager.add_hookspecs(newhooks)
2 changes: 2 additions & 0 deletions tavern/testutils/pytesthook/item.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ def runtest(self):

load_plugins(self.global_cfg)

self.global_cfg["tavern_internal"] = {"pytest_hook_caller": self.config.hook}

# INTERNAL
# NOTE - now that we can 'mark' tests, we could use pytest.mark.xfail
# instead. This doesn't differentiate between an error in verification
Expand Down
21 changes: 21 additions & 0 deletions tavern/testutils/pytesthook/newhooks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# pylint: disable=unused-argument


def pytest_tavern_log_response(response):
"""Called when a response is obtained from a server
Todo:
This just takes 'response' at the moment, will need to be expanded to
take the filename and the test name/stage name as well.
"""


def pytest_tavern_before_request(stage):
"""Called before each request"""


def pytest_tavern_after_request(stage):
"""Called after each request, even if it fails
Note:
Some types of stages can run the request successfully, then fail later
on during verification.
"""
19 changes: 19 additions & 0 deletions tests/integration/test_response_types.tavern.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,22 @@ stages:
response:
status_code: 200
body: null

---

test_name: Test unicode responses as list

includes:
- !include common.yaml

stages:
- name: Echo back a unicode value and make sure it matches
request:
url: "{host}/echo"
method: POST
json:
- 手机号格式不正确
response:
status_code: 200
body:
- 手机号格式不正确
2 changes: 2 additions & 0 deletions tests/unit/conftest.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from mock import Mock
import pytest


Expand All @@ -13,6 +14,7 @@ def fix_example_includes():
},
"backends": {"mqtt": "paho-mqtt", "http": "requests"},
"strict": True,
"tavern_internal": {"pytest_hook_caller": Mock()},
}

return includes.copy()
Loading