|
1 | 1 | """Unit testing suite for the Graphviz plugin.""" |
2 | 2 |
|
3 | 3 | # Copyright (C) 2015, 2021, 2023, 2025 Rafael Laboissière <[email protected]> |
| 4 | +# Copyright (C) 2025 Mark Shroyer <[email protected]> |
4 | 5 | # |
5 | 6 | # This program is free software: you can redistribute it and/or modify it |
6 | 7 | # under the terms of the GNU General Affero Public License as published by |
|
21 | 22 | from tempfile import mkdtemp |
22 | 23 | import unittest |
23 | 24 |
|
| 25 | +from bs4 import BeautifulSoup, Tag |
| 26 | + |
24 | 27 | from pelican import Pelican |
25 | 28 | from pelican.settings import read_settings |
26 | 29 |
|
27 | 30 | from . import graphviz |
28 | 31 |
|
29 | 32 | TEST_FILE_STEM = "test" |
30 | 33 | TEST_DIR_PREFIX = "pelicantests." |
31 | | -GRAPHVIZ_RE = ( |
32 | | - r'<{0} class="{1}"><img alt="{2}" ' |
33 | | - r'src="data:image/svg\+xml;base64,[0-9a-zA-Z+=]+"></{0}>' |
34 | | -) |
35 | | - |
36 | | -GRAPHVIZ_RE_XML = r'<svg width="\d+pt" height="\d+pt"' |
| 34 | +DIMENSION_ATTR_RE = re.compile(r"\d+pt") |
37 | 35 |
|
38 | 36 |
|
39 | 37 | class TestGraphviz(unittest.TestCase): |
40 | 38 | """Class for testing the URL output of the Graphviz plugin.""" |
41 | 39 |
|
42 | 40 | def setUp( |
43 | 41 | self, |
44 | | - block_start="..graphviz", |
45 | | - image_class="graphviz", |
46 | | - html_element="div", |
47 | | - alt_text="GRAPH", |
48 | | - compress=True, |
49 | | - options=None, |
50 | | - expected_html_element=None, |
51 | | - expected_image_class=None, |
52 | | - expected_alt_text=None, |
53 | | - digraph_id="G", |
| 42 | + config=None, |
| 43 | + settings=None, |
| 44 | + expected=None, |
54 | 45 | ): |
55 | 46 | """Set up the test environment.""" |
56 | 47 | # Set the paths for the input (content) and output (html) files |
57 | 48 | self.output_path = mkdtemp(prefix=TEST_DIR_PREFIX) |
58 | 49 | self.content_path = mkdtemp(prefix=TEST_DIR_PREFIX) |
59 | 50 |
|
60 | | - # Configuration setting for the Pelican process |
61 | | - settings = { |
| 51 | + # Input configuration |
| 52 | + self.config = { |
| 53 | + "md_block_start": "..graphviz", |
| 54 | + "options": None, |
| 55 | + "digraph_id": "G", |
| 56 | + } |
| 57 | + if config is not None: |
| 58 | + self.config.update(config) |
| 59 | + |
| 60 | + # Settings for the Pelican process |
| 61 | + self.settings = { |
62 | 62 | "PATH": self.content_path, |
63 | 63 | "OUTPUT_PATH": self.output_path, |
64 | 64 | "PLUGINS": [graphviz], |
65 | 65 | "CACHE_CONTENT": False, |
66 | | - "GRAPHVIZ_HTML_ELEMENT": html_element, |
67 | | - "GRAPHVIZ_BLOCK_START": block_start, |
68 | | - "GRAPHVIZ_IMAGE_CLASS": image_class, |
69 | | - "GRAPHVIZ_ALT_TEXT": alt_text, |
70 | | - "GRAPHVIZ_COMPRESS": compress, |
71 | 66 | } |
| 67 | + if settings is not None: |
| 68 | + self.settings.update(settings) |
| 69 | + |
| 70 | + # Properties of the expected output |
| 71 | + self.expected = { |
| 72 | + "compressed": True, |
| 73 | + "html_element": "div", |
| 74 | + "image_class": "graphviz", |
| 75 | + "alt_text": "G", |
| 76 | + } |
| 77 | + if expected is not None: |
| 78 | + self.expected.update(expected) |
72 | 79 |
|
73 | | - # Store the image_class and the html_element in self, since they will |
74 | | - # be needed in the test_output method defined below |
75 | | - self.image_class = image_class |
76 | | - self.html_element = html_element |
77 | | - self.alt_text = alt_text |
78 | | - |
79 | | - # Get default expected values |
80 | | - if not expected_image_class: |
81 | | - self.expected_image_class = self.image_class |
82 | | - else: |
83 | | - self.expected_image_class = expected_image_class |
84 | | - if not expected_html_element: |
85 | | - self.expected_html_element = self.html_element |
86 | | - else: |
87 | | - self.expected_html_element = expected_html_element |
88 | | - if not expected_alt_text: |
89 | | - self.expected_alt_text = digraph_id if digraph_id else alt_text |
90 | | - else: |
91 | | - self.expected_alt_text = expected_alt_text |
| 80 | + def test_md(self): |
| 81 | + options_string = "" |
| 82 | + if self.config["options"]: |
| 83 | + kvs = ",".join(f'{k}="{v}"' for k, v in self.config["options"].items()) |
| 84 | + options_string = f"[{kvs}]" |
92 | 85 |
|
93 | 86 | # Create the article file |
94 | 87 | with open(os.path.join(self.content_path, f"{TEST_FILE_STEM}.md"), "w") as fid: |
95 | 88 | # Write header |
96 | 89 | fid.write(f"Title: {TEST_FILE_STEM}\nDate: 1970-01-01\n") |
97 | 90 | # Write Graphviz block |
98 | | - fid.write( |
99 | | - f""" |
100 | | -{block_start}{f" [{options}] " if options else " "}dot |
101 | | -digraph {digraph_id if digraph_id else ""} {{ |
| 91 | + fid.write(f""" |
| 92 | +{self.config["md_block_start"]} {options_string} dot |
| 93 | +digraph{f" {self.config['digraph_id']}" if self.config["digraph_id"] else ""} {{ |
102 | 94 | graph [rankdir = LR]; |
103 | 95 | Hello -> World |
104 | 96 | }} |
105 | | -""" |
106 | | - ) |
| 97 | +""") |
107 | 98 |
|
108 | | - # Run the Pelican instance |
109 | | - self.settings = read_settings(override=settings) |
110 | | - pelican = Pelican(settings=self.settings) |
111 | | - pelican.run() |
| 99 | + self.run_pelican() |
| 100 | + self.assert_expected_output() |
112 | 101 |
|
113 | | - def tearDown(self): |
114 | | - """Tidy up the test environment.""" |
115 | | - rmtree(self.output_path) |
116 | | - rmtree(self.content_path) |
| 102 | + def run_pelican(self): |
| 103 | + settings = read_settings(override=self.settings) |
| 104 | + pelican = Pelican(settings=settings) |
| 105 | + pelican.run() |
117 | 106 |
|
118 | | - def test_output(self): |
| 107 | + def assert_expected_output(self): |
119 | 108 | """Test for default values of the configuration variables.""" |
120 | 109 | # Open the output HTML file |
121 | 110 | with open(os.path.join(self.output_path, f"{TEST_FILE_STEM}.html")) as fid: |
| 111 | + # Keep content as a string so we can see full content in output |
| 112 | + # from failed asserts. |
122 | 113 | content = fid.read() |
123 | | - found = False |
124 | | - # Iterate over the lines and look for the HTML element corresponding |
125 | | - # to the generated Graphviz figure |
126 | | - for line in content.splitlines(): |
127 | | - if self.settings["GRAPHVIZ_COMPRESS"]: |
128 | | - if re.search( |
129 | | - GRAPHVIZ_RE.format( |
130 | | - self.expected_html_element, |
131 | | - self.expected_image_class, |
132 | | - self.expected_alt_text, |
133 | | - ), |
134 | | - line, |
135 | | - ): |
136 | | - found = True |
137 | | - break |
138 | | - elif re.search(GRAPHVIZ_RE_XML, line): |
139 | | - found = True |
140 | | - break |
141 | | - assert found, content |
| 114 | + soup = BeautifulSoup(content, "html.parser") |
| 115 | + if self.expected["compressed"]: |
| 116 | + elt = soup.find( |
| 117 | + self.expected["html_element"], class_=self.expected["image_class"] |
| 118 | + ) |
| 119 | + assert isinstance(elt, Tag), content |
| 120 | + |
| 121 | + img = elt.find("img", attrs={"alt": self.expected["alt_text"]}) |
| 122 | + assert img is not None, content |
| 123 | + else: |
| 124 | + svg = soup.find("svg") |
| 125 | + assert isinstance(svg, Tag), content |
| 126 | + |
| 127 | + for attr in ["width", "height"]: |
| 128 | + assert attr in svg.attrs |
| 129 | + assert DIMENSION_ATTR_RE.fullmatch(str(svg.attrs[attr])) |
| 130 | + |
| 131 | + def tearDown(self): |
| 132 | + """Tidy up the test environment.""" |
| 133 | + rmtree(self.output_path) |
| 134 | + rmtree(self.content_path) |
142 | 135 |
|
143 | 136 |
|
144 | 137 | class TestGraphvizHtmlElement(TestGraphviz): |
145 | 138 | """Class for exercising the configuration variable GRAPHVIZ_HTML_ELEMENT.""" |
146 | 139 |
|
147 | 140 | def setUp(self): |
148 | 141 | """Initialize the configuration.""" |
149 | | - TestGraphviz.setUp(self, html_element="span") |
150 | | - |
151 | | - def test_output(self): |
152 | | - """Test for GRAPHVIZ_HTML_ELEMENT setting.""" |
153 | | - TestGraphviz.test_output(self) |
| 142 | + value = "span" |
| 143 | + super().setUp( |
| 144 | + settings={"GRAPHVIZ_HTML_ELEMENT": value}, |
| 145 | + expected={"html_element": value}, |
| 146 | + ) |
154 | 147 |
|
155 | 148 |
|
156 | 149 | class TestGraphvizBlockStart(TestGraphviz): |
157 | 150 | """Class for exercising the configuration variable GRAPHVIZ_BLOCK_START.""" |
158 | 151 |
|
159 | 152 | def setUp(self): |
160 | 153 | """Initialize the configuration.""" |
161 | | - TestGraphviz.setUp(self, block_start="==foobar") |
162 | | - |
163 | | - def test_output(self): |
164 | | - """Test for GRAPHVIZ_BLOCK_START setting.""" |
165 | | - TestGraphviz.test_output(self) |
| 154 | + value = "==foobar" |
| 155 | + super().setUp( |
| 156 | + config={"md_block_start": value}, |
| 157 | + settings={"GRAPHVIZ_BLOCK_START": value}, |
| 158 | + ) |
166 | 159 |
|
167 | 160 |
|
168 | 161 | class TestGraphvizImageClass(TestGraphviz): |
169 | 162 | """Class for exercising configuration variable GRAPHVIZ_IMAGE_CLASS.""" |
170 | 163 |
|
171 | 164 | def setUp(self): |
172 | 165 | """Initialize the configuration.""" |
173 | | - TestGraphviz.setUp(self, image_class="foo") |
174 | | - |
175 | | - def test_output(self): |
176 | | - """Test for GRAPHVIZ_IMAGE_CLASS setting.""" |
177 | | - TestGraphviz.test_output(self) |
| 166 | + value = "foo" |
| 167 | + super().setUp( |
| 168 | + settings={"GRAPHVIZ_IMAGE_CLASS": value}, expected={"image_class": value} |
| 169 | + ) |
178 | 170 |
|
179 | 171 |
|
180 | 172 | class TestGraphvizImageNoCompress(TestGraphviz): |
181 | 173 | """Class for exercising configuration variable GRAPHVIZ_COMPRESS.""" |
182 | 174 |
|
183 | 175 | def setUp(self): |
184 | 176 | """Initialize the configuration.""" |
185 | | - TestGraphviz.setUp(self, compress=False) |
186 | | - |
187 | | - def test_output(self): |
188 | | - """Test for GRAPHVIZ_COMPRESS setting.""" |
189 | | - TestGraphviz.test_output(self) |
| 177 | + value = False |
| 178 | + super().setUp( |
| 179 | + settings={"GRAPHVIZ_COMPRESS": value}, expected={"compressed": value} |
| 180 | + ) |
190 | 181 |
|
191 | 182 |
|
192 | 183 | class TestGraphvizLocallyOverrideConfiguration(TestGraphviz): |
193 | 184 | """Class for exercising the override of a configuration variable.""" |
194 | 185 |
|
195 | 186 | def setUp(self): |
196 | 187 | """Initialize the configuration.""" |
197 | | - TestGraphviz.setUp( |
198 | | - self, |
199 | | - html_element="div", |
200 | | - options="html-element=span", |
201 | | - expected_html_element="span", |
| 188 | + value = "span" |
| 189 | + super().setUp( |
| 190 | + config={"options": {"html-element": value}}, |
| 191 | + expected={"html_element": value}, |
202 | 192 | ) |
203 | 193 |
|
204 | | - def test_output(self): |
205 | | - """Test for overrind the configuration.""" |
206 | | - TestGraphviz.test_output(self) |
207 | | - |
208 | 194 |
|
209 | 195 | class TestGraphvizAltText(TestGraphviz): |
210 | 196 | """Class for exercising configuration variable GRAPHVIZ_ALT_TEXT.""" |
211 | 197 |
|
212 | 198 | def setUp(self): |
213 | 199 | """Initialize the configuration.""" |
214 | | - TestGraphviz.setUp(self, alt_text="foo") |
215 | | - |
216 | | - def test_output(self): |
217 | | - """Test for GRAPHVIZ_IMAGE_CLASS setting.""" |
218 | | - TestGraphviz.test_output(self) |
| 200 | + value = "G" |
| 201 | + super().setUp( |
| 202 | + config={"digraph_id": value}, |
| 203 | + settings={"GRAPHVIZ_ALT_TEXT": "foo"}, |
| 204 | + expected={"alt_text": value}, |
| 205 | + ) |
219 | 206 |
|
220 | 207 |
|
221 | 208 | class TestGraphvizAltTextWithoutID(TestGraphviz): |
222 | 209 | """Class for testing the case where the Graphviz element has no id.""" |
223 | 210 |
|
224 | 211 | def setUp(self): |
225 | 212 | """Initialize the configuration.""" |
226 | | - TestGraphviz.setUp( |
227 | | - self, |
228 | | - digraph_id=None, |
229 | | - alt_text="foo", |
| 213 | + value = "foo" |
| 214 | + super().setUp( |
| 215 | + config={"digraph_id": None}, |
| 216 | + settings={"GRAPHVIZ_ALT_TEXT": value}, |
| 217 | + expected={"alt_text": value}, |
230 | 218 | ) |
231 | 219 |
|
232 | | - def test_output(self): |
233 | | - """Test for GRAPHVIZ_IMAGE_CLASS setting.""" |
234 | | - TestGraphviz.test_output(self) |
235 | | - |
236 | 220 |
|
237 | 221 | class TestGraphvizAltTextViaOption(TestGraphviz): |
238 | 222 | """Class for testing the alternative text given via the alt-text option.""" |
239 | 223 |
|
240 | 224 | def setUp(self): |
241 | 225 | """Initialize the configuration.""" |
242 | 226 | text = "A wonderful graph" |
243 | | - TestGraphviz.setUp( |
244 | | - self, |
245 | | - options=f'alt-text="{text}"', |
246 | | - expected_alt_text=text, |
| 227 | + super().setUp( |
| 228 | + config={"options": {"alt-text": text}}, |
| 229 | + expected={"alt_text": text}, |
247 | 230 | ) |
248 | | - |
249 | | - def test_output(self): |
250 | | - """Test for GRAPHVIZ_IMAGE_CLASS setting.""" |
251 | | - TestGraphviz.test_output(self) |
0 commit comments