|
6 | 6 |
|
7 | 7 | import pytest |
8 | 8 | from pydantic import ValidationError |
9 | | -from ruamel.yaml import YAML |
10 | 9 |
|
11 | 10 | from pdfbaker.baker import Baker, BakerOptions |
| 11 | +from pdfbaker.errors import ConfigurationError, DocumentNotFoundError |
12 | 12 | from pdfbaker.logging import TRACE |
13 | 13 |
|
14 | 14 |
|
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.""" |
25 | 17 | options = BakerOptions() |
26 | 18 | assert not options.quiet |
27 | 19 | assert not options.verbose |
28 | 20 | assert not options.trace |
29 | 21 | assert not options.keep_build |
30 | 22 |
|
31 | 23 |
|
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.""" |
34 | 26 | test_cases = [ |
35 | 27 | (BakerOptions(quiet=True), logging.ERROR), |
36 | 28 | (BakerOptions(verbose=True), logging.DEBUG), |
37 | 29 | (BakerOptions(trace=True), TRACE), |
38 | | - (BakerOptions(), logging.INFO), # default |
| 30 | + (BakerOptions(), logging.INFO), |
39 | 31 | ] |
40 | | - |
41 | 32 | examples_config = Path(__file__).parent.parent / "examples" / "examples.yaml" |
42 | 33 | for options, expected_level in test_cases: |
43 | 34 | Baker(examples_config, options=options) |
44 | 35 | assert logging.getLogger().level == expected_level |
45 | 36 |
|
46 | 37 |
|
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.""" |
51 | 40 | config_file = tmp_path / "invalid.yaml" |
52 | 41 | 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: |
55 | 49 | 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") |
56 | 55 |
|
57 | 56 |
|
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.""" |
61 | 59 | 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) |
75 | 69 | 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 |
85 | 95 | finally: |
86 | | - # Clean up test directories |
87 | 96 | if build_dir.exists(): |
88 | 97 | shutil.rmtree(build_dir) |
89 | 98 | if dist_dir.exists(): |
90 | 99 | 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