Skip to content

Commit 032ca47

Browse files
feat: add ability for top document in a YAML file to merge down
chore: remove redundant code and update merge logic feat: add support for merging test specs feat: add logging for merging initial block in YamlFile feat: add test for redirecting loops feat: Add unit test for multiple documents in YAML file fix: Import Mock in test_files.py fix: handle errors in YamlFile class test: refactor test file handling and add multiple document support refactor: Move Opener class outside of TestGenerateFiles test: add exception handling and improve test structure docs: Add docstring to test_reraise_exception function fix: Simplify exception test description
1 parent 27cec13 commit 032ca47

File tree

4 files changed

+149
-12
lines changed

4 files changed

+149
-12
lines changed

tavern/_core/dict_util.py

+3-10
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,6 @@ def deep_dict_merge(initial_dct: dict, merge_dct: Mapping) -> dict:
260260
dict_merge recurses down into dicts nested to an arbitrary depth
261261
and returns the merged dict. Keys values present in merge_dct take
262262
precedence over values in initial_dct.
263-
Modified from: https://gist.github.com/angstwad/bf22d1822c38a92ec0a9
264263
265264
Params:
266265
initial_dct: dict onto which the merge is executed
@@ -269,15 +268,9 @@ def deep_dict_merge(initial_dct: dict, merge_dct: Mapping) -> dict:
269268
Returns:
270269
recursively merged dict
271270
"""
272-
dct = initial_dct.copy()
273-
274-
for k in merge_dct:
275-
if k in dct and isinstance(dct[k], dict) and isinstance(merge_dct[k], Mapping):
276-
dct[k] = deep_dict_merge(dct[k], merge_dct[k])
277-
else:
278-
dct[k] = merge_dct[k]
279-
280-
return dct
271+
initial_box = Box(initial_dct)
272+
initial_box.merge_update(merge_dct)
273+
return dict(initial_box)
281274

282275

283276
def check_expected_keys(expected: Collection, actual: Collection) -> None:

tavern/_core/pytest/file.py

+17-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from typing import Any, Union
88

99
import pytest
10-
import yaml
10+
import yaml.parser
1111
from box import Box
1212
from pytest import Mark
1313

@@ -365,16 +365,31 @@ def collect(self) -> Iterator[YamlItem]:
365365
except yaml.parser.ParserError as e:
366366
raise exceptions.BadSchemaError from e
367367

368-
for test_spec in all_tests:
368+
merge_down = None
369+
for i, test_spec in enumerate(all_tests):
369370
if not test_spec:
370371
logger.warning("Empty document in input file '%s'", self.path)
371372
continue
372373

374+
if i == 0 and not test_spec.get("stages"):
375+
if test_spec.get("name"):
376+
logger.warning("initial block had no stages, but had a name")
377+
merge_down = test_spec
378+
logger.info(
379+
f"merging initial block from {self.path} into subsequent tests"
380+
)
381+
continue
382+
383+
if merge_down:
384+
test_spec = deep_dict_merge(test_spec, merge_down)
385+
373386
try:
374387
for i in self._generate_items(test_spec):
375388
i.initialise_fixture_attrs()
376389
yield i
377390
except (TypeError, KeyError) as e:
391+
# If there was one of these errors, we can probably figure out
392+
# if the error is from a bad test layout by calling verify_tests
378393
try:
379394
verify_tests(test_spec, with_plugins=False)
380395
except Exception as e2:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
includes:
3+
- !include common.yaml
4+
---
5+
test_name: Test redirecting loops
6+
7+
stages:
8+
- name: Expect a 302 without setting the flag
9+
max_retries: 2
10+
request:
11+
follow_redirects: true
12+
url: "{host}/redirect/loop"
13+
response:
14+
status_code: 200

tests/unit/test_files.py

+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import contextlib
2+
import dataclasses
3+
import pathlib
4+
import tempfile
5+
from collections.abc import Callable, Generator
6+
from typing import Any
7+
from unittest.mock import Mock
8+
9+
import pytest
10+
import yaml
11+
12+
from tavern._core import exceptions
13+
from tavern._core.pytest.file import YamlFile
14+
from tavern._core.pytest.item import YamlItem
15+
16+
17+
@pytest.fixture(scope="function")
18+
def tavern_test_content():
19+
"""return some example tests"""
20+
21+
test_docs = [
22+
{"test_name": "First test", "stages": [{"name": "stage 1"}]},
23+
{"test_name": "Second test", "stages": [{"name": "stage 2"}]},
24+
{"test_name": "Third test", "stages": [{"name": "stage 3"}]},
25+
]
26+
27+
return test_docs
28+
29+
30+
@contextlib.contextmanager
31+
def tavern_test_file(test_content: list[Any]) -> Generator[pathlib.Path, Any, None]:
32+
"""Create a temporary YAML file with multiple documents"""
33+
34+
with tempfile.TemporaryDirectory() as tmpdir:
35+
file_path = pathlib.Path(tmpdir) / "test.yaml"
36+
37+
# Write the documents to the file
38+
with file_path.open("w", encoding="utf-8") as f:
39+
for doc in test_content:
40+
yaml.dump(doc, f)
41+
f.write("---\n")
42+
43+
yield file_path
44+
45+
46+
@dataclasses.dataclass
47+
class Opener:
48+
"""Simple mock for generating items because pytest makes it hard to wrap
49+
their internal functionality"""
50+
51+
path: pathlib.Path
52+
_generate_items: Callable[[dict], Any]
53+
54+
55+
class TestGenerateFiles:
56+
@pytest.mark.parametrize("with_merge_down_test", (True, False))
57+
def test_multiple_documents(self, tavern_test_content, with_merge_down_test):
58+
"""Verify that multiple documents in a YAML file result in multiple tests"""
59+
60+
# Collect all tests
61+
if with_merge_down_test:
62+
tavern_test_content.insert(0, {"includes": []})
63+
64+
def generate_yamlitem(test_spec):
65+
mock = Mock(spec=YamlItem)
66+
mock.name = test_spec["test_name"]
67+
yield mock
68+
69+
with tavern_test_file(tavern_test_content) as filename:
70+
tests = list(
71+
YamlFile.collect(
72+
Opener(
73+
path=filename,
74+
_generate_items=generate_yamlitem,
75+
)
76+
)
77+
)
78+
79+
assert len(tests) == 3
80+
81+
# Verify each test has the correct name
82+
expected_names = ["First test", "Second test", "Third test"]
83+
for test, expected_name in zip(tests, expected_names):
84+
assert test.name == expected_name
85+
86+
@pytest.mark.parametrize(
87+
"content, exception",
88+
(
89+
({"kookdff": "?A?A??"}, exceptions.BadSchemaError),
90+
({"test_name": "name", "stages": [{"name": "lflfl"}]}, TypeError),
91+
),
92+
)
93+
def test_reraise_exception(
94+
self, tavern_test_content, content: dict, exception: BaseException
95+
):
96+
"""Verify that exceptions are properly reraised when loading YAML test files.
97+
98+
Test that when an exception occurs during test generation, it is properly
99+
reraised as a schema error if the schema is bad."""
100+
101+
def raise_error(test_spec):
102+
raise TypeError
103+
104+
tavern_test_content.insert(0, content)
105+
106+
with tavern_test_file(tavern_test_content) as filename:
107+
with pytest.raises(exception):
108+
list(
109+
YamlFile.collect(
110+
Opener(
111+
path=filename,
112+
_generate_items=raise_error,
113+
)
114+
)
115+
)

0 commit comments

Comments
 (0)