From 9c4df1c44cf317bc1a7cfafc2d4c463aafaf1245 Mon Sep 17 00:00:00 2001 From: Michael Boulton Date: Thu, 12 Jan 2023 15:27:53 +0000 Subject: [PATCH 01/12] Add finally block (rebased) --- tavern/_core/exceptions.py | 9 ++ tavern/_core/plugins.py | 42 +++-- tavern/_core/run.py | 144 ++++++++++-------- tavern/_core/schema/tests.jsonschema.yaml | 7 + .../integration/test_control_flow.tavern.yaml | 53 +++++++ 5 files changed, 176 insertions(+), 79 deletions(-) create mode 100644 tests/integration/test_control_flow.tavern.yaml diff --git a/tavern/_core/exceptions.py b/tavern/_core/exceptions.py index 45ea4980a..472071e69 100644 --- a/tavern/_core/exceptions.py +++ b/tavern/_core/exceptions.py @@ -1,6 +1,15 @@ +from typing import TYPE_CHECKING, Dict, Optional + +if TYPE_CHECKING: + from tavern._core.pytest.config import TestConfig + + class TavernException(Exception): """Base exception""" + stage: Optional[Dict] + test_block_config: Optional["TestConfig"] + class BadSchemaError(TavernException): """Schema mismatch""" diff --git a/tavern/_core/plugins.py b/tavern/_core/plugins.py index e7c2dc739..a3f0e8029 100644 --- a/tavern/_core/plugins.py +++ b/tavern/_core/plugins.py @@ -4,11 +4,14 @@ significantly if/when a proper plugin system is implemented! """ import logging +from typing import Any, Dict import stevedore from tavern._core import exceptions from tavern._core.dict_util import format_keys +from tavern._core.pytest.config import TestConfig +from tavern.request import BaseRequest logger = logging.getLogger(__name__) @@ -161,15 +164,17 @@ def get_extra_sessions(test_spec, test_block_config): return sessions -def get_request_type(stage, test_block_config, sessions): +def get_request_type( + stage: dict, test_block_config: TestConfig, sessions: Dict[str, PluginHelperBase] +) -> BaseRequest: """Get the request object for this stage there can only be one Args: - stage (dict): spec for this stage - test_block_config (dict): variables for this test run - sessions (dict): all available sessions + stage: spec for this stage + test_block_config: variables for this test run + sessions: all available sessions Returns: BaseRequest: request object with a run() method @@ -217,12 +222,12 @@ class ResponseVerifier(dict): plugin_name: str -def _foreach_response(stage, test_block_config, action): +def _foreach_response(stage: dict, test_block_config: TestConfig, action): """Do something for each response Args: - stage (dict): Stage of test - test_block_config (dict): Config for test + stage : Stage of test + test_block_config : Config for test action ((p: {plugin, name}, response_block: dict) -> Any): function that takes (plugin, response block) Returns: @@ -241,7 +246,9 @@ def _foreach_response(stage, test_block_config, action): return retvals -def get_expected(stage, test_block_config, sessions): +def get_expected( + stage: dict, test_block_config: TestConfig, sessions: Dict[str, PluginHelperBase] +): """Get expected responses for each type of request Though only 1 request can be made, it can cause multiple responses. @@ -251,9 +258,9 @@ def get_expected(stage, test_block_config, sessions): BEFORE running the request. Args: - stage (dict): test stage - test_block_config (dict): available configuration for this test - sessions (dict): all available sessions + stage: test stage + test_block_config: available configuration for this test + sessions : all available sessions Returns: dict: mapping of request type: expected response dict @@ -273,13 +280,18 @@ def action(p, response_block): return _foreach_response(stage, test_block_config, action) -def get_verifiers(stage, test_block_config, sessions, expected): +def get_verifiers( + stage: dict, + test_block_config: TestConfig, + sessions: Dict[str, PluginHelperBase], + expected: Dict[str, Any], +): """Get one or more response validators for this stage Args: - stage (dict): spec for this stage - test_block_config (dict): variables for this test run - sessions (dict): all available sessions + stage: spec for this stage + test_block_config: variables for this test run + sessions: all available sessions expected (dict): expected responses for this stage Returns: diff --git a/tavern/_core/run.py b/tavern/_core/run.py index 61860af0e..8978bc154 100644 --- a/tavern/_core/run.py +++ b/tavern/_core/run.py @@ -1,21 +1,25 @@ from contextlib import ExitStack import copy from copy import deepcopy +import dataclasses from distutils.util import strtobool # pylint: disable=deprecated-module import functools import logging +from typing import Dict from tavern._core import exceptions from tavern._core.plugins import ( + PluginHelperBase, get_expected, get_extra_sessions, get_request_type, get_verifiers, ) -from tavern._core.strict_util import StrictLevel +from tavern._core.strict_util import StrictLevel, StrictSetting from .dict_util import format_keys, get_tavern_box from .pytest import call_hook +from .pytest.config import TestConfig from .report import attach_stage_content, wrap_step from .testhelpers import delay, retry @@ -122,7 +126,7 @@ def run_test(in_file, test_spec, global_cfg): # Initialise test config for this test with the global configuration before # starting test_block_config = global_cfg.copy() - default_global_stricness = global_cfg.strict + default_global_strictness = global_cfg.strict tavern_box = get_tavern_box() @@ -162,41 +166,25 @@ def getonly(stage): has_only = any(getonly(stage) for stage in test_spec["stages"]) - # Run tests in a path in order - for idx, stage in enumerate(test_spec["stages"]): - if stage.get("skip"): - continue - if has_only and not getonly(stage): - continue - - test_block_config = test_block_config.with_strictness( - default_global_stricness - ) - test_block_config = test_block_config.with_strictness( - _calculate_stage_strictness(stage, test_block_config, test_spec) - ) - - # Wrap run_stage with retry helper - run_stage_with_retries = retry(stage, test_block_config)(run_stage) - - partial = functools.partial( - run_stage_with_retries, sessions, stage, test_block_config - ) + runner = _TestRunner( + default_global_strictness, sessions, test_block_config, test_spec + ) - allure_name = "Stage {}: {}".format( - idx, format_keys(stage["name"], test_block_config.variables) - ) - step = wrap_step(allure_name, partial) + try: + # Run tests in a path in order + for idx, stage in enumerate(test_spec["stages"]): + if stage.get("skip"): + continue + if has_only and not getonly(stage): + continue - try: - step() - except exceptions.TavernException as e: - e.stage = stage - e.test_block_config = test_block_config - raise + runner.wrap_run_stage(idx, stage) - if getonly(stage): - break + if getonly(stage): + break + finally: + for idx, stage in enumerate(test_spec.get("finally", [])): + runner.wrap_run_stage(idx, stage) def _calculate_stage_strictness(stage, test_block_config, test_spec): @@ -259,45 +247,73 @@ def update_stage_options(new_option): return new_strict -def run_stage(sessions, stage, test_block_config): - """Run one stage from the test +@dataclasses.dataclass(frozen=True) +class _TestRunner: + default_global_strictness: StrictSetting + sessions: Dict[str, PluginHelperBase] + test_block_config: TestConfig + test_spec: Dict - Args: - sessions (dict): Dictionary of relevant 'session' objects used for this test - stage (dict): specification of stage to be run - test_block_config (TestConfig): available variables for test - """ - stage = copy.deepcopy(stage) - name = stage["name"] + def wrap_run_stage(self, idx: int, stage): + stage_config = self.test_block_config.with_strictness( + self.default_global_strictness + ) + stage_config = stage_config.with_strictness( + _calculate_stage_strictness(stage, stage_config, self.test_spec) + ) + # Wrap run_stage with retry helper + run_stage_with_retries = retry(stage, stage_config)(self.run_stage) + partial = functools.partial(run_stage_with_retries, stage, stage_config) + allure_name = "Stage {}: {}".format( + idx, format_keys(stage["name"], stage_config.variables) + ) + step = wrap_step(allure_name, partial) - attach_stage_content(stage) + try: + step() + except exceptions.TavernException as e: + e.stage = stage + e.test_block_config = stage_config + raise - r = get_request_type(stage, test_block_config, sessions) + def run_stage(self, stage: dict, stage_config: TestConfig): + """Run one stage from the test - tavern_box = test_block_config.variables["tavern"] - tavern_box.update(request_vars=r.request_vars) + Args: + stage: specification of stage to be run + stage_config: available variables for test + """ + stage = copy.deepcopy(stage) + name = stage["name"] - expected = get_expected(stage, test_block_config, sessions) + attach_stage_content(stage) - delay(stage, "before", test_block_config.variables) + r = get_request_type(stage, stage_config, self.sessions) - logger.info("Running stage : %s", name) + tavern_box = stage_config.variables["tavern"] + tavern_box.update(request_vars=r.request_vars) - call_hook( - test_block_config, - "pytest_tavern_beta_before_every_request", - request_args=r.request_vars, - ) + expected = get_expected(stage, stage_config, self.sessions) + + delay(stage, "before", stage_config.variables) + + logger.info("Running stage : %s", name) + + call_hook( + stage_config, + "pytest_tavern_beta_before_every_request", + request_args=r.request_vars, + ) - verifiers = get_verifiers(stage, test_block_config, sessions, expected) + verifiers = get_verifiers(stage, stage_config, self.sessions, expected) - response = r.run() + response = r.run() - for response_type, response_verifiers in verifiers.items(): - logger.debug("Running verifiers for %s", response_type) - for v in response_verifiers: - saved = v.verify(response) - test_block_config.variables.update(saved) + for response_type, response_verifiers in verifiers.items(): + logger.debug("Running verifiers for %s", response_type) + for v in response_verifiers: + saved = v.verify(response) + stage_config.variables.update(saved) - tavern_box.pop("request_vars") - delay(stage, "after", test_block_config.variables) + tavern_box.pop("request_vars") + delay(stage, "after", stage_config.variables) diff --git a/tavern/_core/schema/tests.jsonschema.yaml b/tavern/_core/schema/tests.jsonschema.yaml index c31d043db..37e347c38 100644 --- a/tavern/_core/schema/tests.jsonschema.yaml +++ b/tavern/_core/schema/tests.jsonschema.yaml @@ -423,3 +423,10 @@ properties: oneOf: - $ref: "#/definitions/stage" - $ref: "#/definitions/stage_ref" + + finally: + type: array + description: Stages to run after test finishes + + items: + $ref: "#/definitions/stage" diff --git a/tests/integration/test_control_flow.tavern.yaml b/tests/integration/test_control_flow.tavern.yaml new file mode 100644 index 000000000..e945c12c7 --- /dev/null +++ b/tests/integration/test_control_flow.tavern.yaml @@ -0,0 +1,53 @@ +--- + +test_name: Test finally block doing nothing + +stages: + - name: Simple echo + request: + url: "{host}/echo" + method: POST + json: + value: "123" + response: + status_code: 200 + json: + value: "123" + +finally: + - name: nothing + request: + url: "{host}/echo" + method: POST + json: + value: "123" + +--- + +test_name: Test finally block fail + +_xfail: run + +stages: + - name: Simple echo + request: + url: "{host}/echo" + method: POST + json: + value: "123" + response: + status_code: 200 + json: + value: "123" + +finally: + - name: nothing + request: + url: "{host}/echo" + method: DELETE + json: + value: "123" + response: + status_code: 200 + json: + value: "123" From e19614db614be3eea89360cac098f8091d600083 Mon Sep 17 00:00:00 2001 From: Michael Boulton Date: Sun, 4 Jun 2023 12:35:57 +0100 Subject: [PATCH 02/12] Fix host --- tests/integration/test_control_flow.tavern.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/test_control_flow.tavern.yaml b/tests/integration/test_control_flow.tavern.yaml index a353a9382..9af51ab80 100644 --- a/tests/integration/test_control_flow.tavern.yaml +++ b/tests/integration/test_control_flow.tavern.yaml @@ -4,7 +4,7 @@ test_name: Test finally block doing nothing stages: - name: Simple echo request: - url: "{host}/echo" + url: "{global_host}/echo" method: POST json: value: "123" @@ -16,7 +16,7 @@ stages: finally: - name: nothing request: - url: "{host}/echo" + url: "{global_host}/echo" method: POST json: value: "123" From f077e5c3c1200f2be0c07dda9ea6e0b45bc0615a Mon Sep 17 00:00:00 2001 From: Michael Boulton Date: Sun, 25 Jun 2023 17:18:06 +0100 Subject: [PATCH 03/12] Update python to 3.11 --- tox-integration.ini | 2 +- tox.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tox-integration.ini b/tox-integration.ini index 10598e92b..444459436 100644 --- a/tox-integration.ini +++ b/tox-integration.ini @@ -4,7 +4,7 @@ skip_missing_interpreters = true isolated_build = True [testenv] -basepython = python3.10 +basepython = python3.11 passenv = DOCKER_TLS_VERIFY DOCKER_HOST DOCKER_CERT_PATH DOCKER_BUILDKIT setenv = TEST_HOST = http://localhost:5003 diff --git a/tox.ini b/tox.ini index 70494974e..68cf47daf 100644 --- a/tox.ini +++ b/tox.ini @@ -5,7 +5,7 @@ isolated_build = True [testenv] passenv = XDG_CACHE_HOME -basepython = python3.10 +basepython = python3.11 whitelist_externals = mypy install_command = python -m pip install {opts} {packages} -c constraints.txt From 4abae05bbf949ac73db89418e3168a78efad5ecb Mon Sep 17 00:00:00 2001 From: Michael Boulton Date: Sun, 25 Jun 2023 17:18:30 +0100 Subject: [PATCH 04/12] Add ability to tell if it failed during a finally stage --- tavern/_core/exceptions.py | 6 +++++- tavern/_core/pytest/item.py | 9 +++++++-- tavern/_core/run.py | 5 +++-- tavern/_core/schema/jsonschema.py | 2 ++ tavern/_core/schema/tests.jsonschema.yaml | 1 + tavern/_core/schema/tests.schema.yaml | 1 + tests/integration/test_control_flow.tavern.yaml | 6 +++--- 7 files changed, 22 insertions(+), 8 deletions(-) diff --git a/tavern/_core/exceptions.py b/tavern/_core/exceptions.py index 06309de0e..3caf3a9a0 100644 --- a/tavern/_core/exceptions.py +++ b/tavern/_core/exceptions.py @@ -5,10 +5,14 @@ class TavernException(Exception): - """Base exception""" + """Base exception + + Fields are internal and might change in future + """ stage: Optional[Dict] test_block_config: Optional["TestConfig"] + is_final: bool = False class BadSchemaError(TavernException): diff --git a/tavern/_core/pytest/item.py b/tavern/_core/pytest/item.py index 2baff2fe2..56ed08a9d 100644 --- a/tavern/_core/pytest/item.py +++ b/tavern/_core/pytest/item.py @@ -216,10 +216,15 @@ def runtest(self) -> None: logger.info("xfailing test while verifying schema") self.add_marker(pytest.mark.xfail, True) raise - except exceptions.TavernException: - if xfail == "run": + except exceptions.TavernException as e: + if xfail == "run" and not e.is_final: logger.info("xfailing test when running") self.add_marker(pytest.mark.xfail, True) + elif xfail == "finally" and e.is_final: + logger.critical(self.spec) + logger.info("xfailing test when finalising") + self.add_marker(pytest.mark.xfail, True) + raise else: if xfail: diff --git a/tavern/_core/run.py b/tavern/_core/run.py index 9a7de90fb..d85a4ec40 100644 --- a/tavern/_core/run.py +++ b/tavern/_core/run.py @@ -194,7 +194,7 @@ def getonly(stage): break finally: for idx, stage in enumerate(test_spec.get("finally", [])): - runner.run_stage(idx, stage) + runner.run_stage(idx, stage, is_final=True) def _calculate_stage_strictness( @@ -266,7 +266,7 @@ class _TestRunner: test_block_config: TestConfig test_spec: Mapping - def run_stage(self, idx: int, stage): + def run_stage(self, idx: int, stage, *, is_final: bool = False): stage_config = self.test_block_config.with_strictness( self.default_global_strictness ) @@ -286,6 +286,7 @@ def run_stage(self, idx: int, stage): except exceptions.TavernException as e: e.stage = stage e.test_block_config = stage_config + e.is_final = is_final raise def wrapped_run_stage(self, stage: dict, stage_config: TestConfig): diff --git a/tavern/_core/schema/jsonschema.py b/tavern/_core/schema/jsonschema.py index 944dc0105..896a80ce4 100644 --- a/tavern/_core/schema/jsonschema.py +++ b/tavern/_core/schema/jsonschema.py @@ -163,6 +163,8 @@ def verify_jsonschema(to_verify, schema) -> None: """ ) + logger.debug("original exception from jsonschema: %s", e) + msg = "\n---\n" + "\n---\n".join([str(i) for i in real_context]) raise BadSchemaError(msg) from None diff --git a/tavern/_core/schema/tests.jsonschema.yaml b/tavern/_core/schema/tests.jsonschema.yaml index 2e80ad458..decb652ab 100644 --- a/tavern/_core/schema/tests.jsonschema.yaml +++ b/tavern/_core/schema/tests.jsonschema.yaml @@ -382,6 +382,7 @@ properties: enum: - verify - run + - finally marks: type: array diff --git a/tavern/_core/schema/tests.schema.yaml b/tavern/_core/schema/tests.schema.yaml index a7992d16e..4cc26391d 100644 --- a/tavern/_core/schema/tests.schema.yaml +++ b/tavern/_core/schema/tests.schema.yaml @@ -309,6 +309,7 @@ mapping: enum: - verify - run + - finally strict: func: check_strict_key diff --git a/tests/integration/test_control_flow.tavern.yaml b/tests/integration/test_control_flow.tavern.yaml index 9af51ab80..dbcdfcf14 100644 --- a/tests/integration/test_control_flow.tavern.yaml +++ b/tests/integration/test_control_flow.tavern.yaml @@ -24,12 +24,12 @@ finally: --- test_name: Test finally block fail -_xfail: run +_xfail: finally stages: - name: Simple echo request: - url: "{host}/echo" + url: "{global_host}/echo" method: POST json: value: "123" @@ -41,7 +41,7 @@ stages: finally: - name: nothing request: - url: "{host}/echo" + url: "{global_host}/echo" method: DELETE json: value: "123" From 2b896d38668c31a97f267654157bf0724b253dca Mon Sep 17 00:00:00 2001 From: Michael Boulton Date: Sat, 1 Jul 2023 21:27:13 +0100 Subject: [PATCH 05/12] Revert "Update python to 3.11" This reverts commit f077e5c3c1200f2be0c07dda9ea6e0b45bc0615a. --- tox-integration.ini | 2 +- tox.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tox-integration.ini b/tox-integration.ini index 444459436..10598e92b 100644 --- a/tox-integration.ini +++ b/tox-integration.ini @@ -4,7 +4,7 @@ skip_missing_interpreters = true isolated_build = True [testenv] -basepython = python3.11 +basepython = python3.10 passenv = DOCKER_TLS_VERIFY DOCKER_HOST DOCKER_CERT_PATH DOCKER_BUILDKIT setenv = TEST_HOST = http://localhost:5003 diff --git a/tox.ini b/tox.ini index 68cf47daf..70494974e 100644 --- a/tox.ini +++ b/tox.ini @@ -5,7 +5,7 @@ isolated_build = True [testenv] passenv = XDG_CACHE_HOME -basepython = python3.11 +basepython = python3.10 whitelist_externals = mypy install_command = python -m pip install {opts} {packages} -c constraints.txt From 51ae40d23531fc653eed19107b1dba82294642bc Mon Sep 17 00:00:00 2001 From: Michael Boulton Date: Sat, 1 Jul 2023 21:54:40 +0100 Subject: [PATCH 06/12] Don't need tox travis any more --- constraints.txt | 4 ---- pyproject.toml | 1 - requirements.txt | 6 ------ 3 files changed, 11 deletions(-) diff --git a/constraints.txt b/constraints.txt index e4e1d2e77..12400c99e 100644 --- a/constraints.txt +++ b/constraints.txt @@ -259,10 +259,6 @@ tomli==2.0.1 tomli-w==1.0.0 # via flit tox==3.28.0 - # via - # tavern (pyproject.toml) - # tox-travis -tox-travis==0.13 # via tavern (pyproject.toml) twine==4.0.2 # via tavern (pyproject.toml) diff --git a/pyproject.toml b/pyproject.toml index fd4ef5b54..a1d6107a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,7 +77,6 @@ dev = [ "pytest-xdist", "py", "tox>=3,<4", - "tox-travis", "twine", "wheel", "types-setuptools", diff --git a/requirements.txt b/requirements.txt index ced87e5d7..cf2a636f9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -948,12 +948,6 @@ tomli-w==1.0.0 \ tox==3.28.0 \ --hash=sha256:57b5ab7e8bb3074edc3c0c0b4b192a4f3799d3723b2c5b76f1fa9f2d40316eea \ --hash=sha256:d0d28f3fe6d6d7195c27f8b054c3e99d5451952b54abdae673b71609a581f640 - # via - # tavern (pyproject.toml) - # tox-travis -tox-travis==0.13 \ - --hash=sha256:3e1e4868d108748012f78cd0bd64f05b5a12b33809d6721a0b35cfb00986e55e \ - --hash=sha256:71fa355d84d32b592428ac8016f669a7c63e459fa42774a33d60072d3d7371dc # via tavern (pyproject.toml) twine==4.0.2 \ --hash=sha256:929bc3c280033347a00f847236564d1c52a3e61b1ac2516c97c48f3ceab756d8 \ From d74d26123ea049b350b60a0ac19f01ff0d2d6648 Mon Sep 17 00:00:00 2001 From: Michael Boulton Date: Sat, 1 Jul 2023 22:02:56 +0100 Subject: [PATCH 07/12] Just run ruff --- example/advanced/server.py | 12 ++++++------ example/components/server.py | 2 +- example/cookies/server.py | 8 ++++---- example/generate_from_openapi/pub_tavern.py | 5 +++-- example/hooks/conftest.py | 2 +- example/hooks/server.py | 1 - example/mqtt/conftest.py | 3 +-- example/mqtt/listener.py | 1 - example/mqtt/server.py | 5 ++--- example/simple/server.py | 1 - 10 files changed, 18 insertions(+), 22 deletions(-) diff --git a/example/advanced/server.py b/example/advanced/server.py index c9b399a72..726cd99e2 100644 --- a/example/advanced/server.py +++ b/example/advanced/server.py @@ -1,9 +1,10 @@ -import sqlite3 +import contextlib import datetime import functools -from flask import Flask, jsonify, request, g -import jwt +import sqlite3 +import jwt +from flask import Flask, g, jsonify, request app = Flask(__name__) @@ -19,12 +20,11 @@ def get_db(): db = g._database = sqlite3.connect(DATABASE) with db: - try: + with contextlib.suppress(Exception): db.execute( "CREATE TABLE numbers_table (name TEXT NOT NULL, number INTEGER NOT NULL)" ) - except Exception: - pass + return db diff --git a/example/components/server.py b/example/components/server.py index b7792c456..3bbc59bc1 100644 --- a/example/components/server.py +++ b/example/components/server.py @@ -1,8 +1,8 @@ import datetime import functools -from flask import Flask, jsonify, request import jwt +from flask import Flask, jsonify, request app = Flask(__name__) diff --git a/example/cookies/server.py b/example/cookies/server.py index e7629a062..9026f1381 100644 --- a/example/cookies/server.py +++ b/example/cookies/server.py @@ -1,7 +1,8 @@ +import contextlib import functools import sqlite3 -from flask import Flask, jsonify, request, g, session +from flask import Flask, g, jsonify, request, session app = Flask(__name__) app.secret_key = "t1uNraxw+9oxUyCuXHO2G0u38ig=" @@ -17,12 +18,11 @@ def get_db(): db = g._database = sqlite3.connect(DATABASE) with db: - try: + with contextlib.suppress(Exception): db.execute( "CREATE TABLE numbers_table (name TEXT NOT NULL, number INTEGER NOT NULL)" ) - except: - pass + return db diff --git a/example/generate_from_openapi/pub_tavern.py b/example/generate_from_openapi/pub_tavern.py index 371a82841..33fd60f94 100644 --- a/example/generate_from_openapi/pub_tavern.py +++ b/example/generate_from_openapi/pub_tavern.py @@ -1,7 +1,8 @@ import sys from urllib.parse import urlparse -from coreapi import Client + import yaml +from coreapi import Client def generate_tavern_yaml(json_path): @@ -68,7 +69,7 @@ def display_help(): print( "eg: pub_tavern.py https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/examples/v2.0/json/petstore-simple.json" ) - exit(-1) + sys.exit(-1) if __name__ == "__main__": diff --git a/example/hooks/conftest.py b/example/hooks/conftest.py index 76164c7b7..bac833490 100644 --- a/example/hooks/conftest.py +++ b/example/hooks/conftest.py @@ -1,6 +1,6 @@ +import logging import os import tempfile -import logging import pytest diff --git a/example/hooks/server.py b/example/hooks/server.py index 9f965f9b1..91952128f 100644 --- a/example/hooks/server.py +++ b/example/hooks/server.py @@ -1,6 +1,5 @@ from flask import Flask, jsonify, request - app = Flask(__name__) diff --git a/example/mqtt/conftest.py b/example/mqtt/conftest.py index cabeddffa..d75e282a2 100644 --- a/example/mqtt/conftest.py +++ b/example/mqtt/conftest.py @@ -1,4 +1,3 @@ -import datetime import logging import logging.config import random @@ -51,7 +50,7 @@ def setup_logging(): - stderr level: DEBUG propagate: False - tavern: + tavern: <<: *log tavern.mqtt: &reduced_log diff --git a/example/mqtt/listener.py b/example/mqtt/listener.py index 756b4735a..f641831a5 100644 --- a/example/mqtt/listener.py +++ b/example/mqtt/listener.py @@ -3,7 +3,6 @@ import logging.config import os import sqlite3 -import time import paho.mqtt.client as paho import yaml diff --git a/example/mqtt/server.py b/example/mqtt/server.py index c74e2a0c8..bc73c910d 100644 --- a/example/mqtt/server.py +++ b/example/mqtt/server.py @@ -193,10 +193,9 @@ def _reset_db(db): with db: def attempt(query): - try: + with contextlib.suppress(Exception): db.execute(query) - except: - pass + attempt("DELETE FROM devices_table") attempt( diff --git a/example/simple/server.py b/example/simple/server.py index 9f965f9b1..91952128f 100644 --- a/example/simple/server.py +++ b/example/simple/server.py @@ -1,6 +1,5 @@ from flask import Flask, jsonify, request - app = Flask(__name__) From eff34719759a3bd880136dd3d0a55785cd9e8cb1 Mon Sep 17 00:00:00 2001 From: Michael Boulton Date: Sat, 5 Aug 2023 17:46:43 +0100 Subject: [PATCH 08/12] cleanup --- tavern/_core/pytest/item.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tavern/_core/pytest/item.py b/tavern/_core/pytest/item.py index 56ed08a9d..1c3596de9 100644 --- a/tavern/_core/pytest/item.py +++ b/tavern/_core/pytest/item.py @@ -221,7 +221,6 @@ def runtest(self) -> None: logger.info("xfailing test when running") self.add_marker(pytest.mark.xfail, True) elif xfail == "finally" and e.is_final: - logger.critical(self.spec) logger.info("xfailing test when finalising") self.add_marker(pytest.mark.xfail, True) @@ -229,12 +228,6 @@ def runtest(self) -> None: else: if xfail: raise Exception("internal: xfail test did not fail '{}'".format(xfail)) - # else: - # if xfail: - # logger.error("Expected test to fail") - # raise exceptions.TestFailError( - # "Expected test to fail at {} stage".format(xfail) - # ) finally: call_hook( self.global_cfg, From bd8b1276513f40dc91a11a7b08046d72d4ef503d Mon Sep 17 00:00:00 2001 From: Michael Boulton Date: Sat, 5 Aug 2023 18:21:20 +0100 Subject: [PATCH 09/12] Add more tests --- tavern/_core/run.py | 11 ++++- tests/unit/test_core.py | 89 ++++++++++++++++++++++++++++++++++++++++- tox.ini | 2 +- 3 files changed, 99 insertions(+), 3 deletions(-) diff --git a/tavern/_core/run.py b/tavern/_core/run.py index d85a4ec40..2b42e5cfd 100644 --- a/tavern/_core/run.py +++ b/tavern/_core/run.py @@ -193,7 +193,16 @@ def getonly(stage): if getonly(stage): break finally: - for idx, stage in enumerate(test_spec.get("finally", [])): + finally_stages = test_spec.get("finally", []) + if not isinstance(finally_stages, list): + raise exceptions.BadSchemaError( + f"finally block should be a list of dicts but was {type(finally_stages)}" + ) + for idx, stage in enumerate(finally_stages): + if not isinstance(stage, dict): + raise exceptions.BadSchemaError( + f"finally block should be a dict but was {type(stage)}" + ) runner.run_stage(idx, stage, is_final=True) diff --git a/tests/unit/test_core.py b/tests/unit/test_core.py index 32d2b1985..17515389e 100644 --- a/tests/unit/test_core.py +++ b/tests/unit/test_core.py @@ -1,3 +1,4 @@ +import copy import dataclasses import json import os @@ -37,7 +38,7 @@ def fix_example_test(): @pytest.fixture(name="mockargs") def fix_mock_response_args(fulltest): - response = fulltest["stages"][0]["response"] + response = copy.deepcopy(fulltest["stages"][0]["response"]) content = response["json"] args = { @@ -500,6 +501,92 @@ def test_format_request_var_value(self, fulltest, includes): assert pmock.called +class TestFinally: + @staticmethod + def run_test(fulltest, mockargs, includes): + mock_response = Mock(**mockargs) + + with patch( + "tavern._plugins.rest.request.requests.Session.request", + return_value=mock_response, + ) as pmock: + run_test("heif", fulltest, includes) + + assert pmock.called + + return pmock + + @pytest.mark.parametrize("finally_block", ([],)) + def test_nop(self, fulltest, mockargs, includes, finally_block): + """ignore empty finally blocks""" + fulltest["finally"] = finally_block + + self.run_test(fulltest, mockargs, includes) + + @pytest.mark.parametrize( + "finally_block", + ( + {}, + "hi", + 3, + ), + ) + def test_wrong_type(self, fulltest, mockargs, includes, finally_block): + """final stages need to be dicts too""" + fulltest["finally"] = finally_block + + with pytest.raises(exceptions.BadSchemaError): + self.run_test(fulltest, mockargs, includes) + + @pytest.fixture + def finally_request(self): + return { + "name": "step 1", + "request": {"url": "http://www.myfinal.com", "method": "POST"}, + "response": { + "status_code": 200, + "json": {"key": "value"}, + "headers": {"content-type": "application/json"}, + }, + } + + def test_finally_run(self, fulltest, mockargs, includes, finally_request): + fulltest["finally"] = [finally_request] + + pmock = self.run_test(fulltest, mockargs, includes) + + assert pmock.call_count == 2 + assert pmock.mock_calls[1].kwargs.items() >= finally_request["request"].items() + + def test_finally_run_twice(self, fulltest, mockargs, includes, finally_request): + fulltest["finally"] = [finally_request, finally_request] + + pmock = self.run_test(fulltest, mockargs, includes) + + assert pmock.call_count == 3 + assert pmock.mock_calls[1].kwargs.items() >= finally_request["request"].items() + assert pmock.mock_calls[2].kwargs.items() >= finally_request["request"].items() + + def test_finally_run_on_main_failure( + self, fulltest, mockargs, includes, finally_request + ): + fulltest["finally"] = [finally_request] + + mockargs["status_code"] = 503 + + mock_response = Mock(**mockargs) + + with patch( + "tavern._plugins.rest.request.requests.Session.request", + return_value=mock_response, + ) as pmock: + with pytest.raises(exceptions.TestFailError): + run_test("heif", fulltest, includes) + + assert pmock.call_count == 2 + assert pmock.mock_calls[1].kwargs.items() >= finally_request["request"].items() + + def test_copy_config(pytestconfig): cfg_1 = load_global_cfg(pytestconfig) diff --git a/tox.ini b/tox.ini index 40db486f1..91a975e69 100644 --- a/tox.ini +++ b/tox.ini @@ -12,7 +12,7 @@ install_command = python -m pip install {opts} {packages} -c constraints.txt extras = dev commands = - {envbindir}/python -m pytest --cov-report term-missing --cov tavern + {envbindir}/python -m pytest --cov-report term-missing --cov tavern {posargs} [testenv:py3check] commands = From 0222f3e2ce48081a6b7164422f7b2b320683dd64 Mon Sep 17 00:00:00 2001 From: Michael Boulton Date: Sat, 5 Aug 2023 18:31:04 +0100 Subject: [PATCH 10/12] split workflow --- .github/workflows/main.yml | 55 +++++++++++++++++++++++++++++++++----- 1 file changed, 49 insertions(+), 6 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e9da3a1f5..b2a68003b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -24,7 +24,7 @@ jobs: - uses: pre-commit/action@v3.0.0 - test: + unit-tests: runs-on: ubuntu-latest needs: simple-checks @@ -38,6 +38,49 @@ jobs: - TOXENV: py3 TOXCFG: tox.ini + env: + TOXENV: ${{ matrix.TOXENV }} + TOXCFG: ${{ matrix.TOXCFG }} + + steps: + - uses: actions/checkout@v3 + + - uses: actions/cache@v3 + env: + cache-name: cache-${{ matrix.TOXENV }} + with: + path: .tox + key: ${{ runner.os }}-tox-${{ env.cache-name }}-${{ hashFiles('pyproject.toml', 'requirements.in') }} + + - uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('pyproject.toml', 'requirements.in') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.11" + + - name: install deps + run: | + pip install tox -c constraints.txt + + - name: tox + run: | + tox -c ${TOXCFG} -e ${TOXENV} + + integration-tests: + runs-on: ubuntu-latest + needs: unit-tests + + strategy: + fail-fast: false + matrix: + include: + # integration tests - TOXENV: py3-generic TOXCFG: tox-integration.ini - TOXENV: py3-mqtt @@ -56,6 +99,9 @@ jobs: TOXCFG: ${{ matrix.TOXCFG }} steps: + - uses: jpribyl/action-docker-layer-caching@v0.1.1 + continue-on-error: true + - uses: actions/checkout@v3 - uses: actions/cache@v3 @@ -63,7 +109,7 @@ jobs: cache-name: cache-${{ matrix.TOXENV }} with: path: .tox - key: ${{ runner.os }}-tox-${{ env.cache-name }}-${{ hashFiles('tox.ini', 'tox-integration.ini', 'pyproject.toml', 'requirements.in') }} + key: ${{ runner.os }}-tox-${{ env.cache-name }}-${{ hashFiles('pyproject.toml', 'requirements.in') }} - uses: actions/cache@v3 with: @@ -72,9 +118,6 @@ jobs: restore-keys: | ${{ runner.os }}-pip- - - uses: jpribyl/action-docker-layer-caching@v0.1.1 - continue-on-error: true - - name: Set up Python uses: actions/setup-python@v4 with: @@ -86,4 +129,4 @@ jobs: - name: tox run: | - tox -c ${TOXCFG} -e ${TOXENV} + tox -c ${TOXCFG} -e ${TOXENV} \ No newline at end of file From 3568ed1e84b9f1821f5429a82e1994f7793b9986 Mon Sep 17 00:00:00 2001 From: Michael Boulton Date: Sat, 5 Aug 2023 18:32:14 +0100 Subject: [PATCH 11/12] fix lint2 --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b2a68003b..67c81136c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -129,4 +129,4 @@ jobs: - name: tox run: | - tox -c ${TOXCFG} -e ${TOXENV} \ No newline at end of file + tox -c ${TOXCFG} -e ${TOXENV} From 299f2a34f9be39e8e749749434d889157ac36315 Mon Sep 17 00:00:00 2001 From: Michael Boulton Date: Sat, 5 Aug 2023 18:52:31 +0100 Subject: [PATCH 12/12] Add docs --- docs/source/basics.md | 278 +++++++++++++++++++++++++----------------- docs/source/http.md | 2 +- 2 files changed, 167 insertions(+), 113 deletions(-) diff --git a/docs/source/basics.md b/docs/source/basics.md index 11852ede6..ac243d914 100644 --- a/docs/source/basics.md +++ b/docs/source/basics.md @@ -52,17 +52,24 @@ just be to check that an endpoint returns a 401 with no login information. A more complicated one might be: 1. Log in to server - - `POST` login information in body - - Expect login details to be returned in body + +- `POST` login information in body +- Expect login details to be returned in body + 2. Get user information - - `GET` with login information in `Authorization` header - - Expect user information returned in body + +- `GET` with login information in `Authorization` header +- Expect user information returned in body + 3. Create a new resource with that user information - - `POST` with login information in `Authorization` header and user information in body - - Expect a 201 with the created resource in the body + +- `POST` with login information in `Authorization` header and user information in body +- Expect a 201 with the created resource in the body + 4. Make sure it's stored on the server - - `GET` with login information in `Authorization` header - - Expect the same information returned as in the previous step + +- `GET` with login information in `Authorization` header +- Expect the same information returned as in the previous step The **name** of each stage is a description of what is happening in that particular test. @@ -117,11 +124,14 @@ and lists recursively. If the response is: ```json { - "thing": { - "nested": [ - 1, 2, 3, 4 - ] - } + "thing": { + "nested": [ + 1, + 2, + 3, + 4 + ] + } } ``` @@ -148,7 +158,8 @@ response: nested_thing: "thing" ``` -This will save `{"nested": [1, 2, 3, 4]}` into the `nested_thing` variable. See the documentation for the `force_format_include` tag for how this can be used. +This will save `{"nested": [1, 2, 3, 4]}` into the `nested_thing` variable. See the documentation for +the `force_format_include` tag for how this can be used. **NOTE**: The behaviour of these queries used to be different and indexing into an array was done like `thing.nested.0`. This will be deprecated in the @@ -157,7 +168,8 @@ an array was done like `thing.nested.0`. This will be deprecated in the It is also possible to save data using function calls, [explained below](#saving-data-from-a-response). For a more formal definition of the schema that the tests are validated against, -check [tests schema](https://github.com/taverntesting/tavern/blob/master/tavern/schemas/tests.schema.yaml) in the main Tavern repository. +check [tests schema](https://github.com/taverntesting/tavern/blob/master/tavern/schemas/tests.schema.yaml) in the main +Tavern repository. ## Generating Test Reports @@ -167,7 +179,7 @@ to your Pip dependencies and pass the `--alluredir=` flag when running Tave a test report with the stages that were run, the responses, any fixtures used, and any errors. See the [Allure documentation](https://docs.qameta.io/allure/#_installing_a_commandline) for more -information on how to use it. +information on how to use it. ## Variable formatting @@ -361,14 +373,14 @@ response: type: seq required: True sequence: - - type: map - mapping: - user_number: - type: int - required: False - user_name: - type: str - required: True + - type: map + mapping: + user_number: + type: int + required: False + user_name: + type: str + required: True ``` If an external function you are using raises any exception, the test will be @@ -400,6 +412,7 @@ above that this function should _not_ take any arguments): # utils.py from box import Box + def generate_bearer_token(): token = sign_a_jwt() auth_header = { @@ -435,6 +448,7 @@ Input from external functions can be merged into a request instead by specifying def return_hello(): return {"hello": "there"} ``` + ```yaml request: url: "{host}/echo" @@ -445,14 +459,15 @@ def return_hello(): function: ext_functions:return_hello ``` -If `tavern-merge-ext-function-values` is set, this will send "hello" and "goodbye" in -the request. If not, it will just send "hello". +If `tavern-merge-ext-function-values` is set, this will send "hello" and "goodbye" in +the request. If not, it will just send "hello". Example `pytest.ini` setting `tavern-merge-ext-function-values` as an argument. + ```python # pytest.ini [pytest] -addopts = --tavern-merge-ext-function-values +addopts = --tavern - merge - ext - function - values ``` #### Saving data from a response @@ -466,10 +481,10 @@ Say that we have a server which returns a response like this: ```json { - "user": { - "name": "John Smith", - "id": "abcdef12345" - } + "user": { + "name": "John Smith", + "id": "abcdef12345" + } } ``` @@ -480,6 +495,7 @@ that this function should take the response object as the first argument): # utils.py from box import Box + def test_function(response): return Box({"test_user_name": response.json()["user"]["name"]}) ``` @@ -500,7 +516,8 @@ in later requests. #### A more complicated example For a more practical example, the built in `validate_jwt` function also returns the -decoded token as a dictionary wrapped in a [Box](https://pypi.python.org/pypi/python-box/) object, which allows dot-notation +decoded token as a dictionary wrapped in a [Box](https://pypi.python.org/pypi/python-box/) object, which allows +dot-notation access to members. This means that the contents of the token can be used for future requests. Because Tavern will already be in the Python path (because you installed it as a library) you do not need to modify the `PYTHONPATH`. @@ -602,12 +619,12 @@ response: The behaviour of various levels of 'strictness' based on the response: -| Response | strict=on | strict=off | -| ---- | -------- | ------ | -| `{ "first": 1, "second": { "nested": 2 } }` | PASS | PASS | -| `{ "first": 1 }` | FAIL | PASS | -| `{ "first": 1, "second": { "another": 2 } }` | FAIL | FAIL | -| `{ "first": 1, "second": { "nested": 2, "another": 2 } }` | FAIL | PASS | +| Response | strict=on | strict=off | +|-----------------------------------------------------------|-----------|------------| +| `{ "first": 1, "second": { "nested": 2 } }` | PASS | PASS | +| `{ "first": 1 }` | FAIL | PASS | +| `{ "first": 1, "second": { "another": 2 } }` | FAIL | FAIL | +| `{ "first": 1, "second": { "nested": 2, "another": 2 } }` | FAIL | PASS | Turning 'strict' off also means that extra items in lists will be ignored as long as the ones specified in the test response are present. For example, if the @@ -675,7 +692,7 @@ whichever configuration file Pytest is using. ```ini [pytest] -tavern-strict=json:off headers:on +tavern-strict = json:off headers:on ``` #### Per test @@ -966,8 +983,8 @@ stages: status_code: 200 json: location: - road: 123 Fake Street - country: England + road: 123 Fake Street + country: England --- test_name: Make sure giving premium works @@ -1006,7 +1023,6 @@ stages: has_premium: true ``` - ## Including external files Even with being able to use anchors within the same file, there is often some @@ -1057,9 +1073,8 @@ automatically be loaded and available for formatting as before. Multiple include files can be specified. The environment variable TAVERN_INCLUDE can contain a : separated list of -paths to search for include files. Each path in TAVERN_INCLUDE has -environment variables expanded before it is searched. - +paths to search for include files. Each path in TAVERN_INCLUDE has +environment variables expanded before it is searched. ### Including global configuration files @@ -1197,17 +1212,17 @@ might not work as expected: # pytest.ini [pytest] addopts = - # This will work +# This will work --tavern-global-cfg=integration_tests/local_urls.yaml - # This will not! - # --tavern-global-cfg integration_tests/local_urls.yaml +# This will not! +# --tavern-global-cfg integration_tests/local_urls.yaml ``` Instead, use the `tavern-global-cfg` option in your pytest.ini file: ```ini [pytest] -tavern-global-cfg= +tavern-global-cfg = integration_tests/local_urls.yaml ``` @@ -1226,10 +1241,11 @@ every test: $ tavern-ci --tavern-global-cfg common.yaml test_urls.yaml -- test_server.tavern.yaml $ py.test --tavern-global-cfg common.yaml local_docker_urls.yaml -- test_server.tavern.yaml ``` + ```ini # pytest.ini [pytest] -tavern-global-cfg= +tavern-global-cfg = common.yaml test_urls.yaml ``` @@ -1419,7 +1435,6 @@ This is also how things such as strict key checking is controlled via the An example of using `pytest_args` to exit on the first failure: - ```python from tavern.core import run @@ -1455,6 +1470,7 @@ This would match both of these response bodies: ```yaml returned_block: hello ``` + ```yaml returned_block: nested: value @@ -1495,7 +1511,7 @@ third block must start with 4 and the third block must start with 8, 9, "A", or ```yaml - name: Check that uuidv4 is returned request: - url: {host}/get_uuid/v4 + url: { host }/get_uuid/v4 method: GET response: status_code: 200 @@ -1517,31 +1533,31 @@ format `v1.2.3-510c2665d771e1`: ```yaml stages: -- name: get a token by id - request: - url: "{host}/tokens/get" - method: GET - params: - id: 456 - response: - status_code: 200 - json: - code: abc123 - id: 456 - meta: - version: !anystr - hash: 456 - save: - $ext: - function: tavern.helpers:validate_regex - extra_kwargs: - expression: "v(?P[\d\.]+)-[\w\d]+" - in_jmespath: "meta.version" + - name: get a token by id + request: + url: "{host}/tokens/get" + method: GET + params: + id: 456 + response: + status_code: 200 + json: + code: abc123 + id: 456 + meta: + version: !anystr + hash: 456 + save: + $ext: + function: tavern.helpers:validate_regex + extra_kwargs: + expression: "v(?P[\d\.]+)-[\w\d]+" + in_jmespath: "meta.version" ``` This is a more flexible version of the helper which can also be used to save values as in the example. If a named matching group is used as shown above, the saved values - can then be accessed in subsequent stages by using the `regex.` syntax, eg: +can then be accessed in subsequent stages by using the `regex.` syntax, eg: ```yaml - name: Reuse thing specified in first request @@ -1643,7 +1659,7 @@ could be done by response: status_code: 200 # Expect no users - json: [] + json: [ ] ``` Any blocks of JSON that are included this way will not be recursively formatted. @@ -2020,7 +2036,6 @@ variable_. Using the above example, perhaps we just want to test the server works correctly with the items "rotten apple", "fresh orange", and "unripe pear" rather than the 9 combinations listed above. This can be done like this: - ```yaml --- test_name: Test post a new fruit @@ -2031,9 +2046,9 @@ marks: - fruit - edible vals: - - [rotten, apple] - - [fresh, orange] - - [unripe, pear] + - [ rotten, apple ] + - [ fresh, orange ] + - [ unripe, pear ] # NOTE: we can specify a nested list like this as well: # - # - unripe @@ -2056,7 +2071,6 @@ This can be combined with the 'simpler' style of parametrisation as well - for example, to run the above test but also to specify whether the fruit was expensive or cheap: - ```yaml --- test_name: Test post a new fruit and price @@ -2067,9 +2081,9 @@ marks: - fruit - edible vals: - - [rotten, apple] - - [fresh, orange] - - [unripe, pear] + - [ rotten, apple ] + - [ fresh, orange ] + - [ unripe, pear ] - parametrize: key: price vals: @@ -2106,11 +2120,11 @@ test_name: Test sending a list of list of keys where one is not a string marks: - parametrize: key: - - fruit - - colours + - fruit + - colours vals: - - [ apple, [red, green, pink] ] - - [ pear, [yellow, green] ] + - [ apple, [ red, green, pink ] ] + - [ pear, [ yellow, green ] ] stages: - name: Send fruit and colours @@ -2142,28 +2156,28 @@ functions can be used to read values. For example this block will create 6 tests test_name: Test parametrizing random different data types in the same test marks: -- parametrize: - key: value_to_send - vals: - - a - - [b, c] - - more: stuff - - yet: [more, stuff] - - $ext: - function: ext_functions:return_string - - and: this - $ext: - function: ext_functions:return_dict - - # If 'return_dict' returns {"keys: ["a","b","c"]} this results in: - # { - # "and": "this", - # "keys": [ - # "a", - # "b", - # "c" - # ] - # } + - parametrize: + key: value_to_send + vals: + - a + - [ b, c ] + - more: stuff + - yet: [ more, stuff ] + - $ext: + function: ext_functions:return_string + - and: this + $ext: + function: ext_functions:return_dict + + # If 'return_dict' returns {"keys: ["a","b","c"]} this results in: + # { + # "and": "this", + # "keys": [ + # "a", + # "b", + # "c" + # ] + # } ``` As see in the last example, if the `$ext` function returns a dictionary then it will also be merged @@ -2196,6 +2210,7 @@ import pytest import logging import time + @pytest.fixture def server_password(): with open("/path/to/password/file", "r") as pfile: @@ -2203,6 +2218,7 @@ def server_password(): return password + @pytest.fixture(name="time_request") def fix_time_request(): t0 = time.time() @@ -2258,7 +2274,7 @@ There are some limitations on fixtures: Fixtures which are specified as `autouse` can also be used without explicitly using `usefixtures` in a test. This is a good way to essentially precompute a format variable without also having to use an external function or specify a -`usefixtures` block in every test where you need it. +`usefixtures` block in every test where you need it. To do this, just pass the `autouse=True` parameter to your fixtures along with the relevant scope. Using 'session' will evalute the fixture once at the beginning @@ -2313,6 +2329,7 @@ Example usage: ```python import logging + def pytest_tavern_beta_before_every_test_run(test_dict, variables): logging.info("Starting test %s", test_dict["test_name"]) @@ -2321,7 +2338,7 @@ def pytest_tavern_beta_before_every_test_run(test_dict, variables): ### After every test run -This hook is called _after_ execution of each test, regardless of the test +This hook is called _after_ execution of each test, regardless of the test result. The hook can, for example, be used to perform cleanup after the test is run. Example usage: @@ -2329,6 +2346,7 @@ Example usage: ```python import logging + def pytest_tavern_beta_after_every_test_run(test_dict, variables): logging.info("Ending test %s", test_dict["test_name"]) ``` @@ -2350,14 +2368,50 @@ def pytest_tavern_beta_after_every_response(expected, response): ### Before every request This hook is called just before each request with the arguments passed to the request -"function". By default, this is Session.request (from requests) for HTTP and Client.publish -(from paho-mqtt) for MQTT. +"function". By default, this is Session.request (from requests) for HTTP and Client.publish +(from paho-mqtt) for MQTT. Example usage: ```python import logging + def pytest_tavern_beta_before_every_request(request_args): logging.info("Making request: %s", request_args) ``` + +## Finalising stages + +If you need a stage to run after a test runs, whether it passes or fails (for example, to log out of a service or +invalidate a short-lived auth token) you can use the `finally` block: + +```yaml +--- +test_name: Test finally block doing nothing + +stages: + - name: stage 1 + ... + + - name: stage 2 + ... + + - name: stage 3 + ... + +finally: + - name: clean up + request: + url: "{global_host}/cleanup" + method: POST +``` + +The `finally` block accepts a list of stages which will always be run after the rest of the test finishes, whether it +passed or failed. Each stage in run in order - if one of the `finally` stages fails, the rest will not be run. + +In the above example, if "stage 2" fails then the execution order would be: + +- stage 1 +- stage 2 (fails) +- clean up diff --git a/docs/source/http.md b/docs/source/http.md index d73fa77ca..b0d5c60fb 100644 --- a/docs/source/http.md +++ b/docs/source/http.md @@ -171,7 +171,7 @@ stages: request: url: "{host}/expect_cookie" method: GET - cookies: [] + cookies: [ ] response: status_code: 403 json: