Changeset View
Changeset View
Standalone View
Standalone View
plugins/python/plugin_importer/tests/test_plugin_importer.py
1 | """Unit tests for the plugin_importer module. | ||||
---|---|---|---|---|---|
2 | | ||||
3 | Unit tests can either be toplevel functions whose names start with | ||||
4 | `test_`, or they can be class methods on classes derived from | ||||
5 | `unittest.TestCase`; again the method names need to start with `test_`. | ||||
6 | """ | ||||
7 | | ||||
8 | | ||||
1 | import os | 9 | import os | ||
2 | import pytest | 10 | import pytest | ||
3 | from tempfile import TemporaryDirectory | 11 | from tempfile import TemporaryDirectory | ||
4 | from unittest import TestCase | 12 | from unittest import mock, TestCase | ||
5 | 13 | from zipfile import ZipFile | |||
6 | 14 | | |||
7 | from .. plugin_importer import PluginImporter, PluginReadError | 15 | from .. plugin_importer import ( | ||
16 | NoPluginsFoundException, | ||||
17 | PluginImporter, | ||||
18 | PluginReadError, | ||||
19 | ) | ||||
8 | 20 | | |||
9 | 21 | | |||
10 | class PluginImporterTestCase(TestCase): | 22 | class PluginImporterTestCase(TestCase): | ||
23 | """Collection of unit tests for the plugin_importer module. | ||||
24 | | ||||
25 | Since the tests need a bit of setup for file system operations, we gather | ||||
26 | them together in a TestCase class. | ||||
27 | """ | ||||
11 | 28 | | |||
12 | def setUp(self): | 29 | def setUp(self): | ||
30 | """This method will be run before each test in this class. | ||||
31 | | ||||
32 | We create temporary directories for creating and extracting | ||||
33 | zip files: `resources_dir` will be the destination (a stand-in | ||||
34 | for Krita's resources dir), `plugin_dir` the source where our | ||||
35 | plugin zip is located. | ||||
36 | """ | ||||
37 | | ||||
13 | self.resources_dir = TemporaryDirectory() | 38 | self.resources_dir = TemporaryDirectory() | ||
14 | self.plugin_dir = TemporaryDirectory() | 39 | self.plugin_dir = TemporaryDirectory() | ||
15 | 40 | | |||
16 | def tearDown(self): | 41 | def tearDown(self): | ||
42 | """This method will be run after each test in this class. | ||||
43 | | ||||
44 | We remove the temporary directories created in `setUp`. | ||||
45 | """ | ||||
46 | | ||||
17 | self.resources_dir.cleanup() | 47 | self.resources_dir.cleanup() | ||
18 | self.plugin_dir.cleanup() | 48 | self.plugin_dir.cleanup() | ||
19 | 49 | | |||
20 | @property | 50 | @property | ||
21 | def zip_filename(self): | 51 | def zip_filename(self): | ||
22 | return os.path.join( | 52 | """Helper method for easy access to the full path of the zipped | ||
23 | self.resources_dir.name, 'plugin.zip') | 53 | plugin. | ||
54 | """ | ||||
55 | return os.path.join(self.plugin_dir.name, 'plugin.zip') | ||||
56 | | ||||
57 | def zip_plugin(self, dirname): | ||||
58 | """Helper method to zip a plugin from the subdirectory `dirname` in | ||||
59 | the `fixtures` folder and store it in the temporary directory | ||||
60 | `self.plugin_dir`. | ||||
61 | | ||||
62 | The `fixtures` directory contains the non-zipped plugins for easier | ||||
63 | maintenance. | ||||
64 | """ | ||||
65 | | ||||
66 | # Get the full name of the folder this file resides in: | ||||
67 | testroot = os.path.dirname(os.path.realpath(__file__)) | ||||
68 | | ||||
69 | # From that, we can get the full name of the plugin fixture folder: | ||||
70 | src = os.path.join(testroot, 'fixtures', dirname) | ||||
71 | | ||||
72 | # Zip it: | ||||
73 | with ZipFile(self.zip_filename, 'w') as plugin_zip: | ||||
74 | for root, dirs, files in os.walk(src): | ||||
75 | dirname = root.replace(src, '') | ||||
76 | for filename in files + dirs: | ||||
77 | plugin_zip.write( | ||||
78 | filename=os.path.join(root, filename), | ||||
79 | arcname=os.path.join(dirname, filename)) | ||||
80 | | ||||
81 | def assert_in_resources_dir(self, *path): | ||||
82 | """Helper method to check whether a directory or file exists inside | ||||
83 | `self.resources_dir`. | ||||
84 | """ | ||||
85 | | ||||
86 | assert os.path.exists(os.path.join(self.resources_dir.name, *path)) | ||||
87 | | ||||
88 | def assert_not_in_resources_dir(self, *path): | ||||
89 | """Helper method to check whether a directory or file doesn't exist | ||||
90 | inside `self.resources_dir`. | ||||
91 | """ | ||||
92 | | ||||
93 | assert not os.path.exists(os.path.join(self.resources_dir.name, *path)) | ||||
94 | | ||||
95 | ############################################################ | ||||
96 | # The actual tests start below | ||||
24 | 97 | | |||
25 | def test_zipfile_doesnt_exist(self): | 98 | def test_zipfile_doesnt_exist(self): | ||
99 | """Test: Import a filename that doesn't exist.""" | ||||
100 | | ||||
101 | # Create nothing here... | ||||
102 | | ||||
26 | with pytest.raises(PluginReadError): | 103 | with pytest.raises(PluginReadError): | ||
104 | # We expect an exception | ||||
27 | PluginImporter(self.zip_filename, | 105 | PluginImporter(self.zip_filename, | ||
28 | self.resources_dir.name, | 106 | self.resources_dir.name, | ||
29 | lambda x: True) | 107 | lambda x: True) | ||
30 | 108 | | |||
31 | def test_zipfile_not_a_zip(self): | 109 | def test_zipfile_not_a_zip(self): | ||
110 | """Test: Import a file that isn't a zip file.""" | ||||
111 | | ||||
112 | # Create a text file: | ||||
32 | with open(self.zip_filename, 'w') as f: | 113 | with open(self.zip_filename, 'w') as f: | ||
33 | f.write('foo') | 114 | f.write('foo') | ||
115 | | ||||
34 | with pytest.raises(PluginReadError): | 116 | with pytest.raises(PluginReadError): | ||
117 | # We expect an exception | ||||
35 | PluginImporter(self.zip_filename, | 118 | PluginImporter(self.zip_filename, | ||
36 | self.resources_dir.name, | 119 | self.resources_dir.name, | ||
37 | lambda x: True) | 120 | lambda x: True) | ||
121 | | ||||
122 | def test_simple_plugin_success(self): | ||||
123 | """Test: Import a basic plugin.""" | ||||
124 | | ||||
125 | self.zip_plugin('success_simple') | ||||
126 | importer = PluginImporter(self.zip_filename, | ||||
127 | self.resources_dir.name, | ||||
128 | lambda x: True) | ||||
129 | imported = importer.import_all() | ||||
130 | assert len(imported) == 1 | ||||
131 | self.assert_in_resources_dir('pykrita', 'foo') | ||||
132 | self.assert_in_resources_dir('pykrita', 'foo', '__init__.py') | ||||
133 | self.assert_in_resources_dir('pykrita', 'foo', 'foo.py') | ||||
134 | self.assert_in_resources_dir('pykrita', 'foo.desktop') | ||||
135 | self.assert_in_resources_dir('actions', 'foo.action') | ||||
136 | | ||||
137 | def test_toplevel_plugin_success(self): | ||||
138 | """Test: Import a plugin with everything at toplevel.""" | ||||
139 | | ||||
140 | self.zip_plugin('success_toplevel') | ||||
141 | importer = PluginImporter(self.zip_filename, | ||||
142 | self.resources_dir.name, | ||||
143 | lambda x: True) | ||||
144 | imported = importer.import_all() | ||||
145 | assert len(imported) == 1 | ||||
146 | self.assert_in_resources_dir('pykrita', 'foo') | ||||
147 | self.assert_in_resources_dir('pykrita', 'foo', '__init__.py') | ||||
148 | self.assert_in_resources_dir('pykrita', 'foo', 'foo.py') | ||||
149 | self.assert_in_resources_dir('pykrita', 'foo.desktop') | ||||
150 | self.assert_in_resources_dir('actions', 'foo.action') | ||||
151 | | ||||
152 | def test_nested_plugin_success(self): | ||||
153 | """Test: Import a plugin with nested directories.""" | ||||
154 | | ||||
155 | self.zip_plugin('success_nested') | ||||
156 | importer = PluginImporter(self.zip_filename, | ||||
157 | self.resources_dir.name, | ||||
158 | lambda x: True) | ||||
159 | imported = importer.import_all() | ||||
160 | assert len(imported) == 1 | ||||
161 | self.assert_in_resources_dir('pykrita', 'foo') | ||||
162 | self.assert_in_resources_dir('pykrita', 'foo', '__init__.py') | ||||
163 | self.assert_in_resources_dir('pykrita', 'foo', 'foo.py') | ||||
164 | self.assert_in_resources_dir('pykrita', 'foo.desktop') | ||||
165 | self.assert_in_resources_dir('actions', 'foo.action') | ||||
166 | | ||||
167 | def test_no_action_success(self): | ||||
168 | """Test: Import a plugin without action file. | ||||
169 | | ||||
170 | Should import fine without creating an action file.""" | ||||
171 | | ||||
172 | self.zip_plugin('success_no_action') | ||||
173 | importer = PluginImporter(self.zip_filename, | ||||
174 | self.resources_dir.name, | ||||
175 | lambda x: True) | ||||
176 | imported = importer.import_all() | ||||
177 | assert len(imported) == 1 | ||||
178 | self.assert_in_resources_dir('pykrita', 'foo') | ||||
179 | self.assert_in_resources_dir('pykrita', 'foo', '__init__.py') | ||||
180 | self.assert_in_resources_dir('pykrita', 'foo', 'foo.py') | ||||
181 | self.assert_in_resources_dir('pykrita', 'foo.desktop') | ||||
182 | self.assert_not_in_resources_dir('actions', 'foo.action') | ||||
183 | | ||||
184 | def test_overwrite_existing(self): | ||||
185 | """Test: Overwrite an existing plugin when overwrite confirmed.""" | ||||
186 | | ||||
187 | self.zip_plugin('success_simple') | ||||
188 | | ||||
189 | # Create an existing python module in the the resources directory: | ||||
190 | plugin_dir = os.path.join(self.resources_dir.name, 'pykrita', 'foo') | ||||
191 | os.makedirs(plugin_dir) | ||||
192 | init_file = os.path.join(plugin_dir, '__init__.py') | ||||
193 | with open(init_file, 'w') as f: | ||||
194 | f.write('# existing module') | ||||
195 | | ||||
196 | # Create a mock callback on which we can test that it has been called: | ||||
197 | confirm_callback = mock.MagicMock(return_value=True) | ||||
198 | importer = PluginImporter(self.zip_filename, | ||||
199 | self.resources_dir.name, | ||||
200 | confirm_callback) | ||||
201 | imported = importer.import_all() | ||||
202 | assert len(imported) == 1 | ||||
203 | assert confirm_callback.called | ||||
204 | | ||||
205 | # Existing plugin should be overwritten: | ||||
206 | with open(init_file, 'r') as f: | ||||
207 | assert f.read().strip() == "print('hello')" | ||||
208 | | ||||
209 | def test_dont_overwrite_existing(self): | ||||
210 | """Test: Don't overwrite an existing plugin when overwrite not | ||||
211 | confirmed.""" | ||||
212 | | ||||
213 | self.zip_plugin('success_simple') | ||||
214 | | ||||
215 | # Create an existing python module in the the resources directory: | ||||
216 | plugin_dir = os.path.join(self.resources_dir.name, 'pykrita', 'foo') | ||||
217 | os.makedirs(plugin_dir) | ||||
218 | init_file = os.path.join(plugin_dir, '__init__.py') | ||||
219 | with open(init_file, 'w') as f: | ||||
220 | f.write('# existing module') | ||||
221 | | ||||
222 | # Create a mock callback on which we can test that it has been called: | ||||
223 | confirm_callback = mock.MagicMock(return_value=False) | ||||
224 | importer = PluginImporter(self.zip_filename, | ||||
225 | self.resources_dir.name, | ||||
226 | confirm_callback) | ||||
227 | imported = importer.import_all() | ||||
228 | assert len(imported) == 0 | ||||
229 | assert confirm_callback.called | ||||
230 | | ||||
231 | # Existing plugin should not be overwritten: | ||||
232 | with open(init_file, 'r') as f: | ||||
233 | assert f.read().strip() == '# existing module' | ||||
234 | | ||||
235 | def test_missing_desktop_file(self): | ||||
236 | """Test: Import plugin without a desktop file.""" | ||||
237 | | ||||
238 | self.zip_plugin('fail_no_desktop_file') | ||||
239 | importer = PluginImporter(self.zip_filename, | ||||
240 | self.resources_dir.name, | ||||
241 | lambda x: True) | ||||
242 | with pytest.raises(NoPluginsFoundException): | ||||
243 | # We expect an exception | ||||
244 | importer.import_all() | ||||
245 | | ||||
246 | def test_unparsable_desktop_file(self): | ||||
247 | """Test: Import plugin whose desktop file is not parsable.""" | ||||
248 | | ||||
249 | self.zip_plugin('fail_unparsable_desktop_file') | ||||
250 | importer = PluginImporter(self.zip_filename, | ||||
251 | self.resources_dir.name, | ||||
252 | lambda x: True) | ||||
253 | with pytest.raises(PluginReadError): | ||||
254 | # We expect an exception | ||||
255 | importer.import_all() | ||||
256 | | ||||
257 | def test_missing_keys_in_desktop_file(self): | ||||
258 | """Test: Import plugin whose destkop file is missing needed keys.""" | ||||
259 | | ||||
260 | self.zip_plugin('fail_missing_keys_desktop_file') | ||||
261 | importer = PluginImporter(self.zip_filename, | ||||
262 | self.resources_dir.name, | ||||
263 | lambda x: True) | ||||
264 | with pytest.raises(PluginReadError): | ||||
265 | # We expect an exception | ||||
266 | importer.import_all() | ||||
267 | | ||||
268 | def test_no_matching_plugindir(self): | ||||
269 | """Test: Import plugin whose destkop file is missing needed keys.""" | ||||
270 | | ||||
271 | self.zip_plugin('fail_no_matching_plugindir') | ||||
272 | importer = PluginImporter(self.zip_filename, | ||||
273 | self.resources_dir.name, | ||||
274 | lambda x: True) | ||||
275 | with pytest.raises(NoPluginsFoundException): | ||||
276 | # We expect an exception | ||||
277 | importer.import_all() | ||||
278 | | ||||
279 | def test_no_init_file(self): | ||||
280 | """Test: Import plugin whose python module is missing the __init__.py | ||||
281 | file.""" | ||||
282 | | ||||
283 | self.zip_plugin('fail_no_init_file') | ||||
284 | importer = PluginImporter(self.zip_filename, | ||||
285 | self.resources_dir.name, | ||||
286 | lambda x: True) | ||||
287 | with pytest.raises(NoPluginsFoundException): | ||||
288 | # We expect an exception | ||||
289 | importer.import_all() | ||||
290 | | ||||
291 | def test_unparsable_action_file(self): | ||||
292 | """Test: Import plugin whose action file isn't parsable.""" | ||||
293 | | ||||
294 | self.zip_plugin('fail_unparsable_action_file') | ||||
295 | importer = PluginImporter(self.zip_filename, | ||||
296 | self.resources_dir.name, | ||||
297 | lambda x: True) | ||||
298 | with pytest.raises(PluginReadError): | ||||
299 | # We expect an exception | ||||
300 | importer.import_all() |