Skip to content

Commit 11e8bb8

Browse files
committed
fix: Improved test suite, tests now also linted
Reviewed all tests. Now 73 tests covering 91%.
1 parent 015fc47 commit 11e8bb8

File tree

11 files changed

+882
-448
lines changed

11 files changed

+882
-448
lines changed

pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ source = ["pdfbaker"]
5454

5555
[tool.pylint.main]
5656
py-version = "3.11"
57-
ignore-paths = ["tests/"]
5857
init-hook = "import sys; sys.path.insert(0, 'src')"
5958

6059
[tool.pylint.messages_control]

src/pdfbaker/render.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,18 +48,25 @@ def render(self, *args: Any, **kwargs: Any) -> str:
4848

4949

5050
def render_highlight(rendered: str, **kwargs: Any) -> str:
51-
"""Apply highlight tags to the rendered template content.
51+
"""
52+
Apply highlight tags to the rendered template content.
5253
53-
Convert <highlight> tags to styled <tspan> elements with the highlight color.
54+
Recursively convert all <highlight> tags to styled <tspan> elements
55+
with the highlight color from the `style.highlight_color` setting.
5456
"""
5557
if "style" in kwargs and "highlight_color" in kwargs["style"]:
5658
highlight_color = kwargs["style"]["highlight_color"]
5759

60+
pattern = re.compile(r"<highlight>(.*?)</highlight>", re.DOTALL)
61+
5862
def replacer(match: re.Match[str]) -> str:
59-
content = match.group(1)
63+
# Recursively process the content for nested highlights
64+
content = render_highlight(match.group(1), **kwargs)
6065
return f'<tspan style="fill:{highlight_color}">{content}</tspan>'
6166

62-
rendered = re.sub(r"<highlight>(.*?)</highlight>", replacer, rendered)
67+
# Keep replacing until no more <highlight> tags are found
68+
while pattern.search(rendered):
69+
rendered = pattern.sub(replacer, rendered)
6370

6471
return rendered
6572

tests/conftest.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from pathlib import Path
44

55
import pytest
6+
from ruamel.yaml import YAML
67

78
from pdfbaker.config import Directories
89

@@ -14,8 +15,20 @@ def default_directories(tmp_path: Path) -> Directories:
1415
base=tmp_path,
1516
build=tmp_path / "build",
1617
dist=tmp_path / "dist",
17-
documents=tmp_path / "documents",
18+
documents=tmp_path / "docs",
1819
pages=tmp_path / "pages",
1920
templates=tmp_path / "templates",
2021
images=tmp_path / "images",
2122
)
23+
24+
25+
@pytest.fixture
26+
def write_yaml():
27+
"""Reusable YAML writer for tests."""
28+
29+
def _write_yaml(path, data):
30+
yaml = YAML(typ="full")
31+
with open(path, "w", encoding="utf-8") as file:
32+
yaml.dump(data, file)
33+
34+
return _write_yaml

tests/test_baker.py

Lines changed: 194 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -6,85 +6,232 @@
66

77
import pytest
88
from pydantic import ValidationError
9-
from ruamel.yaml import YAML
109

1110
from pdfbaker.baker import Baker, BakerOptions
11+
from pdfbaker.errors import ConfigurationError, DocumentNotFoundError
1212
from pdfbaker.logging import TRACE
1313

1414

15-
def write_yaml(path, data):
16-
"""Write data to a YAML file using ruamel.yaml."""
17-
yaml = YAML()
18-
with open(path, "w", encoding="utf-8") as file:
19-
yaml.dump(data, file)
20-
21-
22-
# BakerOptions tests
23-
def test_baker_options_defaults() -> None:
24-
"""Test BakerOptions default values."""
15+
def test_baker_options_defaults():
16+
"""BakerOptions: defaults are all False."""
2517
options = BakerOptions()
2618
assert not options.quiet
2719
assert not options.verbose
2820
assert not options.trace
2921
assert not options.keep_build
3022

3123

32-
def test_baker_options_logging_levels() -> None:
33-
"""Test different logging level configurations."""
24+
def test_baker_options_logging_levels():
25+
"""BakerOptions: logging level is set as expected."""
3426
test_cases = [
3527
(BakerOptions(quiet=True), logging.ERROR),
3628
(BakerOptions(verbose=True), logging.DEBUG),
3729
(BakerOptions(trace=True), TRACE),
38-
(BakerOptions(), logging.INFO), # default
30+
(BakerOptions(), logging.INFO),
3931
]
40-
4132
examples_config = Path(__file__).parent.parent / "examples" / "examples.yaml"
4233
for options, expected_level in test_cases:
4334
Baker(examples_config, options=options)
4435
assert logging.getLogger().level == expected_level
4536

4637

47-
# PDFBaker initialization tests
48-
def test_baker_init_invalid_config(tmp_path: Path) -> None:
49-
"""Test PDFBaker initialization with invalid configuration."""
50-
# Create an invalid config file (missing 'documents' key)
38+
def test_baker_init_invalid_config(tmp_path: Path, write_yaml):
39+
"""Baker: raises ValidationError for missing or invalid config fields."""
5140
config_file = tmp_path / "invalid.yaml"
5241
write_yaml(config_file, {"title": "test", "directories": {"base": str(tmp_path)}})
53-
54-
with pytest.raises(ValidationError, match=".*documents.*missing.*"):
42+
with pytest.raises(ValidationError) as exc_info:
43+
Baker(config_file)
44+
assert "documents" in str(exc_info.value)
45+
write_yaml(
46+
config_file, {"documents": "not_a_list", "directories": {"base": str(tmp_path)}}
47+
)
48+
with pytest.raises(ValidationError) as exc_info:
5549
Baker(config_file)
50+
assert "documents" in str(exc_info.value)
51+
abs_config = Path("/tmp/test_config.yaml")
52+
write_yaml(abs_config, {"documents": [], "directories": {"base": "/tmp"}})
53+
baker = Baker(abs_config)
54+
assert baker.config.directories.base == Path("/tmp")
5655

5756

58-
# PDFBaker functionality tests
59-
def test_baker_examples() -> None:
60-
"""Test baking all examples."""
57+
def test_baker_examples():
58+
"""Baker: bakes all examples and verifies output files."""
6159
test_dir = Path(__file__).parent
62-
examples_config = test_dir.parent / "examples" / "examples.yaml"
63-
64-
# Create test output directories
65-
build_dir = test_dir / "build"
66-
dist_dir = test_dir / "dist"
67-
build_dir.mkdir(exist_ok=True)
68-
dist_dir.mkdir(exist_ok=True)
69-
70-
options = BakerOptions(
71-
quiet=True,
72-
keep_build=True,
73-
)
74-
60+
examples_config_path = test_dir.parent / "examples" / "examples.yaml"
61+
examples_base_dir = examples_config_path.parent
62+
build_dir = examples_base_dir / "build"
63+
dist_dir = examples_base_dir / "dist"
64+
if build_dir.exists():
65+
shutil.rmtree(build_dir)
66+
if dist_dir.exists():
67+
shutil.rmtree(dist_dir)
68+
options = BakerOptions(quiet=False, keep_build=True)
7569
try:
76-
baker = Baker(
77-
examples_config,
78-
options=options,
79-
directories={
80-
"build": str(build_dir),
81-
"dist": str(dist_dir),
82-
},
83-
)
84-
baker.bake()
70+
baker = Baker(examples_config_path, options=options)
71+
success = baker.bake()
72+
assert success, "baker.bake() reported failure"
73+
assert build_dir.exists() and any(build_dir.iterdir())
74+
assert dist_dir.exists() and any(dist_dir.iterdir())
75+
expected_filenames = {
76+
"minimal": "minimal_example.pdf",
77+
"regular": "regular_example.pdf",
78+
"custom_locations": "custom_locations_custom.pdf",
79+
"custom_processing": "xkcd_example.pdf",
80+
}
81+
for doc_spec in baker.config.documents:
82+
doc_name = doc_spec.name
83+
doc_output_dir = dist_dir / doc_name
84+
if doc_name == "variants":
85+
for vp in [
86+
doc_output_dir / "basic_variant.pdf",
87+
doc_output_dir / "premium_variant.pdf",
88+
doc_output_dir / "enterprise_variant.pdf",
89+
]:
90+
assert vp.exists() and vp.stat().st_size > 0
91+
elif doc_name in expected_filenames:
92+
filename = expected_filenames[doc_name]
93+
expected_pdf = doc_output_dir / filename
94+
assert expected_pdf.exists() and expected_pdf.stat().st_size > 0
8595
finally:
86-
# Clean up test directories
8796
if build_dir.exists():
8897
shutil.rmtree(build_dir)
8998
if dist_dir.exists():
9099
shutil.rmtree(dist_dir)
100+
assert not build_dir.exists()
101+
assert not dist_dir.exists()
102+
103+
104+
def test_baker_get_selected_documents_missing(
105+
tmp_path, write_yaml, default_directories
106+
):
107+
"""Baker: _get_selected_documents raises DocumentNotFoundError for missing doc."""
108+
config_file = tmp_path / "baker.yaml"
109+
write_yaml(
110+
config_file,
111+
{
112+
"documents": [{"path": "doc1", "name": "doc1"}],
113+
"directories": default_directories.model_dump(mode="json"),
114+
},
115+
)
116+
baker = Baker(config_file=config_file, options=BakerOptions())
117+
with pytest.raises(DocumentNotFoundError):
118+
baker._get_selected_documents(("not_a_doc",)) # pylint: disable=protected-access
119+
120+
121+
def test_baker_teardown_no_build_dir(tmp_path, write_yaml, default_directories):
122+
"""Baker: teardown does nothing if build dir does not exist."""
123+
config_file = tmp_path / "baker.yaml"
124+
dirs = default_directories.model_dump(mode="json")
125+
write_yaml(
126+
config_file,
127+
{
128+
"documents": [{"path": "doc1", "name": "doc1"}],
129+
"directories": dirs,
130+
},
131+
)
132+
baker = Baker(config_file=config_file, options=BakerOptions())
133+
# build dir does not exist
134+
baker.teardown() # Should not raise
135+
136+
137+
def test_baker_bake_success_and_failure(tmp_path, write_yaml, default_directories):
138+
"""Baker: bake() returns True if all succeed, False if any fail."""
139+
# Success case
140+
config_file = tmp_path / "baker.yaml"
141+
write_yaml(
142+
config_file,
143+
{
144+
"documents": [
145+
{"path": "doc1.yaml", "name": "doc1"},
146+
{"path": "doc2.yaml", "name": "doc2"},
147+
],
148+
"directories": default_directories.model_dump(mode="json"),
149+
},
150+
)
151+
docs_dir = tmp_path / "docs"
152+
docs_dir.mkdir(exist_ok=True)
153+
for doc in ("doc1.yaml", "doc2.yaml"):
154+
pages_dir = docs_dir / "pages"
155+
pages_dir.mkdir(exist_ok=True)
156+
write_yaml(pages_dir / "page1.yaml", {"template": "template.svg"})
157+
templates_dir = docs_dir / "templates"
158+
templates_dir.mkdir(exist_ok=True)
159+
(templates_dir / "template.svg").write_text(
160+
'<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"></svg>'
161+
)
162+
doc_dirs = default_directories.model_dump(mode="json")
163+
doc_dirs["templates"] = str(templates_dir)
164+
doc_dirs["pages"] = str(pages_dir)
165+
write_yaml(
166+
docs_dir / doc,
167+
{
168+
"pages": [{"path": "page1.yaml", "name": "page1"}],
169+
"directories": doc_dirs,
170+
"filename": doc,
171+
},
172+
)
173+
baker = Baker(config_file=config_file, options=BakerOptions(keep_build=True))
174+
assert baker.bake(("doc1", "doc2")) is True
175+
176+
# Failure case: one doc missing
177+
with pytest.raises(DocumentNotFoundError):
178+
baker.bake(("doc1", "not_a_doc"))
179+
180+
181+
def test_baker_process_documents_handles_validation_error(
182+
tmp_path, write_yaml, default_directories
183+
):
184+
"""Baker: _process_documents handles ValidationError and logs error."""
185+
config_file = tmp_path / "baker.yaml"
186+
write_yaml(
187+
config_file,
188+
{
189+
"documents": [
190+
{"path": "doc1.yaml", "name": "doc1"},
191+
],
192+
"directories": default_directories.model_dump(mode="json"),
193+
},
194+
)
195+
docs_dir = tmp_path / "docs"
196+
docs_dir.mkdir(exist_ok=True)
197+
pages_dir = docs_dir / "pages"
198+
pages_dir.mkdir(exist_ok=True)
199+
write_yaml(pages_dir / "page1.yaml", {"template": "template.svg"})
200+
templates_dir = docs_dir / "templates"
201+
templates_dir.mkdir(exist_ok=True)
202+
(templates_dir / "template.svg").write_text(
203+
'<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"></svg>'
204+
)
205+
doc_dirs = default_directories.model_dump(mode="json")
206+
doc_dirs["templates"] = str(templates_dir)
207+
doc_dirs["pages"] = str(pages_dir)
208+
# Write a minimal valid YAML to doc1.yaml
209+
write_yaml(
210+
docs_dir / "doc1.yaml",
211+
{"pages": [], "directories": doc_dirs, "filename": "doc1"},
212+
)
213+
baker = Baker(config_file=config_file, options=BakerOptions(keep_build=True))
214+
with pytest.raises(
215+
ConfigurationError, match='Cannot determine pages of document "doc1"'
216+
):
217+
baker._process_documents(baker.config.documents) # pylint: disable=protected-access
218+
219+
220+
def test_baker_teardown_build_dir_not_empty(tmp_path, write_yaml, default_directories):
221+
"""Baker: teardown logs warning if build dir is not empty and does not remove it."""
222+
config_file = tmp_path / "baker.yaml"
223+
dirs = default_directories.model_dump(mode="json")
224+
build_dir = tmp_path / "build"
225+
build_dir.mkdir()
226+
(build_dir / "dummy.txt").write_text("not empty")
227+
dirs["build"] = str(build_dir)
228+
write_yaml(
229+
config_file,
230+
{
231+
"documents": [{"path": "doc1", "name": "doc1"}],
232+
"directories": dirs,
233+
},
234+
)
235+
baker = Baker(config_file=config_file, options=BakerOptions())
236+
baker.teardown() # Should log a warning and not remove the dir
237+
assert build_dir.exists()

0 commit comments

Comments
 (0)