|
| 1 | +#! python3 # noqa E265 |
| 2 | + |
| 3 | +"""Test build without Material theme installed. |
| 4 | +
|
| 5 | +Usage from the repo root folder: |
| 6 | +
|
| 7 | +.. code-block:: python |
| 8 | +
|
| 9 | + # for whole test |
| 10 | + python -m unittest tests.test_build_without_material |
| 11 | + # for specific test |
| 12 | + python -m unittest tests.test_build_without_material.TestBuildWithoutMaterial.test_build_without_material_theme |
| 13 | +""" |
| 14 | + |
| 15 | +# ############################################################################# |
| 16 | +# ########## Libraries ############# |
| 17 | +# ################################## |
| 18 | + |
| 19 | +# Standard library |
| 20 | +import sys |
| 21 | +import tempfile |
| 22 | +import unittest |
| 23 | +from pathlib import Path |
| 24 | +from unittest.mock import patch |
| 25 | + |
| 26 | +# 3rd party |
| 27 | +import feedparser |
| 28 | + |
| 29 | +# test suite |
| 30 | +from tests.base import BaseTest |
| 31 | + |
| 32 | +# ############################################################################# |
| 33 | +# ########## Globals ############### |
| 34 | +# ################################## |
| 35 | + |
| 36 | +OUTPUT_RSS_FEED_CREATED = "feed_rss_created.xml" |
| 37 | +OUTPUT_RSS_FEED_UPDATED = "feed_rss_updated.xml" |
| 38 | +OUTPUT_JSON_FEED_CREATED = "feed_json_created.json" |
| 39 | +OUTPUT_JSON_FEED_UPDATED = "feed_json_updated.json" |
| 40 | + |
| 41 | + |
| 42 | +# ############################################################################# |
| 43 | +# ########## Classes ############### |
| 44 | +# ################################## |
| 45 | + |
| 46 | + |
| 47 | +class TestBuildWithoutMaterial(BaseTest): |
| 48 | + """Test MkDocs build with RSS plugin when Material theme is not available.""" |
| 49 | + |
| 50 | + # -- Standard methods -------------------------------------------------------- |
| 51 | + @classmethod |
| 52 | + def setUpClass(cls): |
| 53 | + """Executed when module is loaded before any test.""" |
| 54 | + cls.feed_image = "https://upload.wikimedia.org/wikipedia/commons/thumb/4/43/Feed-icon.svg/128px-Feed-icon.svg.png" |
| 55 | + |
| 56 | + def setUp(self): |
| 57 | + """Executed before each test.""" |
| 58 | + pass |
| 59 | + |
| 60 | + def tearDown(self): |
| 61 | + """Executed after each test.""" |
| 62 | + pass |
| 63 | + |
| 64 | + @classmethod |
| 65 | + def tearDownClass(cls): |
| 66 | + """Executed after the last test.""" |
| 67 | + pass |
| 68 | + |
| 69 | + # -- TESTS --------------------------------------------------------- |
| 70 | + def test_build_without_material_theme(self): |
| 71 | + """Test that the plugin works correctly when Material theme is not installed. |
| 72 | +
|
| 73 | + This test simulates the absence of the Material theme by temporarily |
| 74 | + blocking its import, ensuring the RSS plugin gracefully handles the missing |
| 75 | + dependency. |
| 76 | + """ |
| 77 | + # Mock the material module to simulate it not being installed |
| 78 | + with patch.dict(sys.modules, {"material": None}): |
| 79 | + # Also need to mock the submodules |
| 80 | + with patch.dict( |
| 81 | + sys.modules, |
| 82 | + { |
| 83 | + "material.plugins": None, |
| 84 | + "material.plugins.blog": None, |
| 85 | + "material.plugins.blog.plugin": None, |
| 86 | + "material.plugins.blog.structure": None, |
| 87 | + }, |
| 88 | + ): |
| 89 | + with tempfile.TemporaryDirectory() as tmpdirname: |
| 90 | + cli_result = self.build_docs_setup( |
| 91 | + testproject_path="docs", |
| 92 | + mkdocs_yml_filepath=Path("tests/fixtures/mkdocs_minimal.yml"), |
| 93 | + output_path=tmpdirname, |
| 94 | + strict=True, |
| 95 | + ) |
| 96 | + |
| 97 | + if cli_result.exception is not None: |
| 98 | + e = cli_result.exception |
| 99 | + return e |
| 100 | + |
| 101 | + self.assertEqual(cli_result.exit_code, 0) |
| 102 | + self.assertIsNone(cli_result.exception) |
| 103 | + |
| 104 | + # Verify RSS feeds were created |
| 105 | + self.assertTrue( |
| 106 | + Path(tmpdirname).joinpath(OUTPUT_RSS_FEED_CREATED).exists() |
| 107 | + ) |
| 108 | + self.assertTrue( |
| 109 | + Path(tmpdirname).joinpath(OUTPUT_RSS_FEED_UPDATED).exists() |
| 110 | + ) |
| 111 | + |
| 112 | + # Verify feeds are valid |
| 113 | + feed_created = feedparser.parse( |
| 114 | + Path(tmpdirname) / OUTPUT_RSS_FEED_CREATED |
| 115 | + ) |
| 116 | + self.assertEqual(feed_created.bozo, 0) |
| 117 | + |
| 118 | + feed_updated = feedparser.parse( |
| 119 | + Path(tmpdirname) / OUTPUT_RSS_FEED_UPDATED |
| 120 | + ) |
| 121 | + self.assertEqual(feed_updated.bozo, 0) |
| 122 | + |
| 123 | + def test_build_with_material_config_but_theme_not_installed(self): |
| 124 | + """Test build with Material-specific config when the theme is not installed. |
| 125 | +
|
| 126 | + This test uses a configuration file that references Material theme features |
| 127 | + (like blog plugin) but simulates the theme not being installed. The plugin |
| 128 | + should handle this gracefully without crashing. |
| 129 | + """ |
| 130 | + with patch.dict(sys.modules, {"material": None}): |
| 131 | + with patch.dict( |
| 132 | + sys.modules, |
| 133 | + { |
| 134 | + "material.plugins": None, |
| 135 | + "material.plugins.blog": None, |
| 136 | + "material.plugins.blog.plugin": None, |
| 137 | + "material.plugins.blog.structure": None, |
| 138 | + }, |
| 139 | + ): |
| 140 | + with tempfile.TemporaryDirectory() as tmpdirname: |
| 141 | + # Use a config that would normally use Material features |
| 142 | + cli_result = self.build_docs_setup( |
| 143 | + testproject_path="docs", |
| 144 | + mkdocs_yml_filepath=Path("tests/fixtures/mkdocs_complete.yml"), |
| 145 | + output_path=tmpdirname, |
| 146 | + strict=False, # Don't fail on warnings |
| 147 | + ) |
| 148 | + |
| 149 | + if cli_result.exception is not None: |
| 150 | + e = cli_result.exception |
| 151 | + return e |
| 152 | + |
| 153 | + # Build should succeed even without Material |
| 154 | + self.assertEqual(cli_result.exit_code, 0) |
| 155 | + self.assertIsNone(cli_result.exception) |
| 156 | + |
| 157 | + # Verify feeds were created and are valid |
| 158 | + feed_created = feedparser.parse( |
| 159 | + Path(tmpdirname) / OUTPUT_RSS_FEED_CREATED |
| 160 | + ) |
| 161 | + self.assertEqual(feed_created.bozo, 0) |
| 162 | + self.assertGreater(len(feed_created.entries), 0) |
| 163 | + |
| 164 | + def test_page_processing_without_material(self): |
| 165 | + """Test that page processing works correctly without Material theme. |
| 166 | +
|
| 167 | + Ensures that pages are processed correctly and their metadata is extracted |
| 168 | + even when Material-specific features are not available. |
| 169 | + """ |
| 170 | + with patch.dict(sys.modules, {"material": None}): |
| 171 | + with patch.dict( |
| 172 | + sys.modules, |
| 173 | + { |
| 174 | + "material.plugins": None, |
| 175 | + "material.plugins.blog": None, |
| 176 | + "material.plugins.blog.plugin": None, |
| 177 | + "material.plugins.blog.structure": None, |
| 178 | + }, |
| 179 | + ): |
| 180 | + with tempfile.TemporaryDirectory() as tmpdirname: |
| 181 | + # Use complete config which includes pages with authors |
| 182 | + cli_result = self.build_docs_setup( |
| 183 | + testproject_path="docs", |
| 184 | + mkdocs_yml_filepath=Path("tests/fixtures/mkdocs_complete.yml"), |
| 185 | + output_path=tmpdirname, |
| 186 | + strict=False, # Material theme not available |
| 187 | + ) |
| 188 | + |
| 189 | + self.assertEqual(cli_result.exit_code, 0) |
| 190 | + self.assertIsNone(cli_result.exception) |
| 191 | + |
| 192 | + # Parse the created feed |
| 193 | + feed_parsed = feedparser.parse( |
| 194 | + Path(tmpdirname) / OUTPUT_RSS_FEED_CREATED |
| 195 | + ) |
| 196 | + |
| 197 | + # Verify feed has entries |
| 198 | + self.assertGreater( |
| 199 | + len(feed_parsed.entries), |
| 200 | + 0, |
| 201 | + "Feed should contain at least one entry", |
| 202 | + ) |
| 203 | + |
| 204 | + # Verify entries have required fields |
| 205 | + for entry in feed_parsed.entries: |
| 206 | + self.assertIn( |
| 207 | + "title", |
| 208 | + entry, |
| 209 | + f"Entry '{entry.get('title', 'UNKNOWN')}' missing title", |
| 210 | + ) |
| 211 | + self.assertIn( |
| 212 | + "link", entry, f"Entry '{entry.title}' missing link" |
| 213 | + ) |
| 214 | + self.assertIn( |
| 215 | + "description", |
| 216 | + entry, |
| 217 | + f"Entry '{entry.title}' missing description", |
| 218 | + ) |
| 219 | + self.assertIn( |
| 220 | + "published", |
| 221 | + entry, |
| 222 | + f"Entry '{entry.title}' missing published date", |
| 223 | + ) |
| 224 | + |
| 225 | + # Look for the specific page with complete metadata |
| 226 | + page_with_complete_meta = next( |
| 227 | + ( |
| 228 | + e |
| 229 | + for e in feed_parsed.entries |
| 230 | + if e.title == "Page with complete meta" |
| 231 | + ), |
| 232 | + None, |
| 233 | + ) |
| 234 | + |
| 235 | + # This page should exist and have author metadata |
| 236 | + self.assertIsNotNone( |
| 237 | + page_with_complete_meta, |
| 238 | + "Page 'Page with complete meta' should be in the feed. " |
| 239 | + f"Available pages: {', '.join([e.title for e in feed_parsed.entries])}", |
| 240 | + ) |
| 241 | + |
| 242 | + self.assertIn( |
| 243 | + "author", |
| 244 | + page_with_complete_meta, |
| 245 | + "Page 'Page with complete meta' should have author metadata", |
| 246 | + ) |
| 247 | + self.assertIsNotNone( |
| 248 | + page_with_complete_meta.description, |
| 249 | + "Page 'Page with complete meta' should have a description", |
| 250 | + ) |
| 251 | + |
| 252 | + |
| 253 | +# ############################################################################## |
| 254 | +# ##### Stand alone program ######## |
| 255 | +# ################################## |
| 256 | +if __name__ == "__main__": |
| 257 | + unittest.main() |
0 commit comments