OLD | NEW |
| (Empty) |
1 # Copyright (c) 2005 Divmod, Inc. | |
2 # Copyright (c) 2007 Twisted Matrix Laboratories. | |
3 # See LICENSE for details. | |
4 | |
5 """ | |
6 Tests for Twisted plugin system. | |
7 """ | |
8 | |
9 import sys, errno, os, time | |
10 import compileall | |
11 | |
12 from zope.interface import Interface | |
13 | |
14 from twisted.trial import unittest | |
15 from twisted.python.filepath import FilePath | |
16 from twisted.python.util import mergeFunctionMetadata | |
17 | |
18 from twisted import plugin | |
19 | |
20 | |
21 | |
22 class ITestPlugin(Interface): | |
23 """ | |
24 A plugin for use by the plugin system's unit tests. | |
25 | |
26 Do not use this. | |
27 """ | |
28 | |
29 | |
30 | |
31 class ITestPlugin2(Interface): | |
32 """ | |
33 See L{ITestPlugin}. | |
34 """ | |
35 | |
36 | |
37 | |
38 class PluginTestCase(unittest.TestCase): | |
39 """ | |
40 Tests which verify the behavior of the current, active Twisted plugins | |
41 directory. | |
42 """ | |
43 | |
44 def setUp(self): | |
45 """ | |
46 Save C{sys.path} and C{sys.modules}, and create a package for tests. | |
47 """ | |
48 self.originalPath = sys.path[:] | |
49 self.savedModules = sys.modules.copy() | |
50 | |
51 self.root = FilePath(self.mktemp()) | |
52 self.root.createDirectory() | |
53 self.package = self.root.child('mypackage') | |
54 self.package.createDirectory() | |
55 self.package.child('__init__.py').setContent("") | |
56 | |
57 FilePath(__file__).sibling('plugin_basic.py' | |
58 ).copyTo(self.package.child('testplugin.py')) | |
59 | |
60 self.originalPlugin = "testplugin" | |
61 | |
62 sys.path.insert(0, self.root.path) | |
63 import mypackage | |
64 self.module = mypackage | |
65 | |
66 | |
67 def tearDown(self): | |
68 """ | |
69 Restore C{sys.path} and C{sys.modules} to their original values. | |
70 """ | |
71 sys.path[:] = self.originalPath | |
72 sys.modules.clear() | |
73 sys.modules.update(self.savedModules) | |
74 | |
75 | |
76 def _unimportPythonModule(self, module, deleteSource=False): | |
77 modulePath = module.__name__.split('.') | |
78 packageName = '.'.join(modulePath[:-1]) | |
79 moduleName = modulePath[-1] | |
80 | |
81 delattr(sys.modules[packageName], moduleName) | |
82 del sys.modules[module.__name__] | |
83 for ext in ['c', 'o'] + (deleteSource and [''] or []): | |
84 try: | |
85 os.remove(module.__file__ + ext) | |
86 except OSError, ose: | |
87 if ose.errno != errno.ENOENT: | |
88 raise | |
89 | |
90 | |
91 def _clearCache(self): | |
92 """ | |
93 Remove the plugins B{droping.cache} file. | |
94 """ | |
95 self.package.child('dropin.cache').remove() | |
96 | |
97 | |
98 def _withCacheness(meth): | |
99 """ | |
100 This is a paranoid test wrapper, that calls C{meth} 2 times, clear the | |
101 cache, and calls it 2 other times. It's supposed to ensure that the | |
102 plugin system behaves correctly no matter what the state of the cache | |
103 is. | |
104 """ | |
105 def wrapped(self): | |
106 meth(self) | |
107 meth(self) | |
108 self._clearCache() | |
109 meth(self) | |
110 meth(self) | |
111 return mergeFunctionMetadata(meth, wrapped) | |
112 | |
113 | |
114 def test_cache(self): | |
115 """ | |
116 Check that the cache returned by L{plugin.getCache} hold the plugin | |
117 B{testplugin}, and that this plugin has the properties we expect: | |
118 provide L{TestPlugin}, has the good name and description, and can be | |
119 loaded successfully. | |
120 """ | |
121 cache = plugin.getCache(self.module) | |
122 | |
123 dropin = cache[self.originalPlugin] | |
124 self.assertEquals(dropin.moduleName, | |
125 'mypackage.%s' % (self.originalPlugin,)) | |
126 self.assertIn("I'm a test drop-in.", dropin.description) | |
127 | |
128 # Note, not the preferred way to get a plugin by its interface. | |
129 p1 = [p for p in dropin.plugins if ITestPlugin in p.provided][0] | |
130 self.assertIdentical(p1.dropin, dropin) | |
131 self.assertEquals(p1.name, "TestPlugin") | |
132 | |
133 # Check the content of the description comes from the plugin module | |
134 # docstring | |
135 self.assertEquals( | |
136 p1.description.strip(), | |
137 "A plugin used solely for testing purposes.") | |
138 self.assertEquals(p1.provided, [ITestPlugin, plugin.IPlugin]) | |
139 realPlugin = p1.load() | |
140 # The plugin should match the class present in sys.modules | |
141 self.assertIdentical( | |
142 realPlugin, | |
143 sys.modules['mypackage.%s' % (self.originalPlugin,)].TestPlugin) | |
144 | |
145 # And it should also match if we import it classicly | |
146 import mypackage.testplugin as tp | |
147 self.assertIdentical(realPlugin, tp.TestPlugin) | |
148 | |
149 test_cache = _withCacheness(test_cache) | |
150 | |
151 | |
152 def test_plugins(self): | |
153 """ | |
154 L{plugin.getPlugins} should return the list of plugins matching the | |
155 specified interface (here, L{ITestPlugin2}), and these plugins | |
156 should be instances of classes with a C{test} method, to be sure | |
157 L{plugin.getPlugins} load classes correctly. | |
158 """ | |
159 plugins = list(plugin.getPlugins(ITestPlugin2, self.module)) | |
160 | |
161 self.assertEquals(len(plugins), 2) | |
162 | |
163 names = ['AnotherTestPlugin', 'ThirdTestPlugin'] | |
164 for p in plugins: | |
165 names.remove(p.__name__) | |
166 p.test() | |
167 | |
168 test_plugins = _withCacheness(test_plugins) | |
169 | |
170 | |
171 def test_detectNewFiles(self): | |
172 """ | |
173 Check that L{plugin.getPlugins} is able to detect plugins added at | |
174 runtime. | |
175 """ | |
176 FilePath(__file__).sibling('plugin_extra1.py' | |
177 ).copyTo(self.package.child('pluginextra.py')) | |
178 try: | |
179 # Check that the current situation is clean | |
180 self.failIfIn('mypackage.pluginextra', sys.modules) | |
181 self.failIf(hasattr(sys.modules['mypackage'], 'pluginextra'), | |
182 "mypackage still has pluginextra module") | |
183 | |
184 plgs = list(plugin.getPlugins(ITestPlugin, self.module)) | |
185 | |
186 # We should find 2 plugins: the one in testplugin, and the one in | |
187 # pluginextra | |
188 self.assertEquals(len(plgs), 2) | |
189 | |
190 names = ['TestPlugin', 'FourthTestPlugin'] | |
191 for p in plgs: | |
192 names.remove(p.__name__) | |
193 p.test1() | |
194 finally: | |
195 self._unimportPythonModule( | |
196 sys.modules['mypackage.pluginextra'], | |
197 True) | |
198 | |
199 test_detectNewFiles = _withCacheness(test_detectNewFiles) | |
200 | |
201 | |
202 def test_detectFilesChanged(self): | |
203 """ | |
204 Check that if the content of a plugin change, L{plugin.getPlugins} is | |
205 able to detect the new plugins added. | |
206 """ | |
207 FilePath(__file__).sibling('plugin_extra1.py' | |
208 ).copyTo(self.package.child('pluginextra.py')) | |
209 try: | |
210 plgs = list(plugin.getPlugins(ITestPlugin, self.module)) | |
211 # Sanity check | |
212 self.assertEquals(len(plgs), 2) | |
213 | |
214 FilePath(__file__).sibling('plugin_extra2.py' | |
215 ).copyTo(self.package.child('pluginextra.py')) | |
216 | |
217 # Fake out Python. | |
218 self._unimportPythonModule(sys.modules['mypackage.pluginextra']) | |
219 | |
220 # Make sure additions are noticed | |
221 plgs = list(plugin.getPlugins(ITestPlugin, self.module)) | |
222 | |
223 self.assertEquals(len(plgs), 3) | |
224 | |
225 names = ['TestPlugin', 'FourthTestPlugin', 'FifthTestPlugin'] | |
226 for p in plgs: | |
227 names.remove(p.__name__) | |
228 p.test1() | |
229 finally: | |
230 self._unimportPythonModule( | |
231 sys.modules['mypackage.pluginextra'], | |
232 True) | |
233 | |
234 test_detectFilesChanged = _withCacheness(test_detectFilesChanged) | |
235 | |
236 | |
237 def test_detectFilesRemoved(self): | |
238 """ | |
239 Check that when a dropin file is removed, L{plugin.getPlugins} doesn't | |
240 return it anymore. | |
241 """ | |
242 FilePath(__file__).sibling('plugin_extra1.py' | |
243 ).copyTo(self.package.child('pluginextra.py')) | |
244 try: | |
245 # Generate a cache with pluginextra in it. | |
246 list(plugin.getPlugins(ITestPlugin, self.module)) | |
247 | |
248 finally: | |
249 self._unimportPythonModule( | |
250 sys.modules['mypackage.pluginextra'], | |
251 True) | |
252 plgs = list(plugin.getPlugins(ITestPlugin, self.module)) | |
253 self.assertEquals(1, len(plgs)) | |
254 | |
255 test_detectFilesRemoved = _withCacheness(test_detectFilesRemoved) | |
256 | |
257 | |
258 def test_nonexistentPathEntry(self): | |
259 """ | |
260 Test that getCache skips over any entries in a plugin package's | |
261 C{__path__} which do not exist. | |
262 """ | |
263 path = self.mktemp() | |
264 self.failIf(os.path.exists(path)) | |
265 # Add the test directory to the plugins path | |
266 self.module.__path__.append(path) | |
267 try: | |
268 plgs = list(plugin.getPlugins(ITestPlugin, self.module)) | |
269 self.assertEqual(len(plgs), 1) | |
270 finally: | |
271 self.module.__path__.remove(path) | |
272 | |
273 test_nonexistentPathEntry = _withCacheness(test_nonexistentPathEntry) | |
274 | |
275 | |
276 def test_nonDirectoryChildEntry(self): | |
277 """ | |
278 Test that getCache skips over any entries in a plugin package's | |
279 C{__path__} which refer to children of paths which are not directories. | |
280 """ | |
281 path = FilePath(self.mktemp()) | |
282 self.failIf(path.exists()) | |
283 path.touch() | |
284 child = path.child("test_package").path | |
285 self.module.__path__.append(child) | |
286 try: | |
287 plgs = list(plugin.getPlugins(ITestPlugin, self.module)) | |
288 self.assertEqual(len(plgs), 1) | |
289 finally: | |
290 self.module.__path__.remove(child) | |
291 | |
292 test_nonDirectoryChildEntry = _withCacheness(test_nonDirectoryChildEntry) | |
293 | |
294 | |
295 def test_deployedMode(self): | |
296 """ | |
297 The C{dropin.cache} file may not be writable: the cache should still be | |
298 attainable, but an error should be logged to show that the cache | |
299 couldn't be updated. | |
300 """ | |
301 # Generate the cache | |
302 plugin.getCache(self.module) | |
303 | |
304 # Add a new plugin | |
305 FilePath(__file__).sibling('plugin_extra1.py' | |
306 ).copyTo(self.package.child('pluginextra.py')) | |
307 | |
308 os.chmod(self.package.path, 0500) | |
309 # Change the right of dropin.cache too for windows | |
310 os.chmod(self.package.child('dropin.cache').path, 0400) | |
311 self.addCleanup(os.chmod, self.package.path, 0700) | |
312 self.addCleanup(os.chmod, | |
313 self.package.child('dropin.cache').path, 0700) | |
314 | |
315 cache = plugin.getCache(self.module) | |
316 # The new plugin should be reported | |
317 self.assertIn('pluginextra', cache) | |
318 self.assertIn(self.originalPlugin, cache) | |
319 | |
320 errors = self.flushLoggedErrors() | |
321 self.assertEquals(len(errors), 1) | |
322 # Windows report OSError, others IOError | |
323 errors[0].trap(OSError, IOError) | |
324 | |
325 | |
326 | |
327 # This is something like the Twisted plugins file. | |
328 pluginInitFile = """ | |
329 from twisted.plugin import pluginPackagePaths | |
330 __path__.extend(pluginPackagePaths(__name__)) | |
331 __all__ = [] | |
332 """ | |
333 | |
334 def pluginFileContents(name): | |
335 return ( | |
336 "from zope.interface import classProvides\n" | |
337 "from twisted.plugin import IPlugin\n" | |
338 "from twisted.test.test_plugin import ITestPlugin\n" | |
339 "\n" | |
340 "class %s(object):\n" | |
341 " classProvides(IPlugin, ITestPlugin)\n") % (name,) | |
342 | |
343 | |
344 def _createPluginDummy(entrypath, pluginContent, real, pluginModule): | |
345 """ | |
346 Create a plugindummy package. | |
347 """ | |
348 entrypath.createDirectory() | |
349 pkg = entrypath.child('plugindummy') | |
350 pkg.createDirectory() | |
351 if real: | |
352 pkg.child('__init__.py').setContent('') | |
353 plugs = pkg.child('plugins') | |
354 plugs.createDirectory() | |
355 if real: | |
356 plugs.child('__init__.py').setContent(pluginInitFile) | |
357 plugs.child(pluginModule + '.py').setContent(pluginContent) | |
358 return plugs | |
359 | |
360 | |
361 | |
362 class DeveloperSetupTests(unittest.TestCase): | |
363 """ | |
364 These tests verify things about the plugin system without actually | |
365 interacting with the deployed 'twisted.plugins' package, instead creating a | |
366 temporary package. | |
367 """ | |
368 | |
369 def setUp(self): | |
370 """ | |
371 Create a complex environment with multiple entries on sys.path, akin to | |
372 a developer's environment who has a development (trunk) checkout of | |
373 Twisted, a system installed version of Twisted (for their operating | |
374 system's tools) and a project which provides Twisted plugins. | |
375 """ | |
376 self.savedPath = sys.path[:] | |
377 self.savedModules = sys.modules.copy() | |
378 self.fakeRoot = FilePath(self.mktemp()) | |
379 self.fakeRoot.createDirectory() | |
380 self.systemPath = self.fakeRoot.child('system_path') | |
381 self.devPath = self.fakeRoot.child('development_path') | |
382 self.appPath = self.fakeRoot.child('application_path') | |
383 self.systemPackage = _createPluginDummy( | |
384 self.systemPath, pluginFileContents('system'), | |
385 True, 'plugindummy_builtin') | |
386 self.devPackage = _createPluginDummy( | |
387 self.devPath, pluginFileContents('dev'), | |
388 True, 'plugindummy_builtin') | |
389 self.appPackage = _createPluginDummy( | |
390 self.appPath, pluginFileContents('app'), | |
391 False, 'plugindummy_app') | |
392 | |
393 # Now we're going to do the system installation. | |
394 sys.path.extend([x.path for x in [self.systemPath, | |
395 self.appPath]]) | |
396 # Run all the way through the plugins list to cause the | |
397 # L{plugin.getPlugins} generator to write cache files for the system | |
398 # installation. | |
399 self.getAllPlugins() | |
400 self.sysplug = self.systemPath.child('plugindummy').child('plugins') | |
401 self.syscache = self.sysplug.child('dropin.cache') | |
402 # Make sure there's a nice big difference in modification times so that | |
403 # we won't re-build the system cache. | |
404 now = time.time() | |
405 os.utime( | |
406 self.sysplug.child('plugindummy_builtin.py').path, | |
407 (now - 5000,) * 2) | |
408 os.utime(self.syscache.path, (now - 2000,) * 2) | |
409 # For extra realism, let's make sure that the system path is no longer | |
410 # writable. | |
411 self.lockSystem() | |
412 self.resetEnvironment() | |
413 | |
414 | |
415 def lockSystem(self): | |
416 """ | |
417 Lock the system directories, as if they were unwritable by this user. | |
418 """ | |
419 os.chmod(self.sysplug.path, 0555) | |
420 os.chmod(self.syscache.path, 0555) | |
421 | |
422 | |
423 def unlockSystem(self): | |
424 """ | |
425 Unlock the system directories, as if they were writable by this user. | |
426 """ | |
427 os.chmod(self.sysplug.path, 0777) | |
428 os.chmod(self.syscache.path, 0777) | |
429 | |
430 | |
431 def getAllPlugins(self): | |
432 """ | |
433 Get all the plugins loadable from our dummy package, and return their | |
434 short names. | |
435 """ | |
436 # Import the module we just added to our path. (Local scope because | |
437 # this package doesn't exist outside of this test.) | |
438 import plugindummy.plugins | |
439 x = list(plugin.getPlugins(ITestPlugin, plugindummy.plugins)) | |
440 return [plug.__name__ for plug in x] | |
441 | |
442 | |
443 def resetEnvironment(self): | |
444 """ | |
445 Change the environment to what it should be just as the test is | |
446 starting. | |
447 """ | |
448 self.unsetEnvironment() | |
449 sys.path.extend([x.path for x in [self.devPath, | |
450 self.systemPath, | |
451 self.appPath]]) | |
452 | |
453 def unsetEnvironment(self): | |
454 """ | |
455 Change the Python environment back to what it was before the test was | |
456 started. | |
457 """ | |
458 sys.modules.clear() | |
459 sys.modules.update(self.savedModules) | |
460 sys.path[:] = self.savedPath | |
461 | |
462 | |
463 def tearDown(self): | |
464 """ | |
465 Reset the Python environment to what it was before this test ran, and | |
466 restore permissions on files which were marked read-only so that the | |
467 directory may be cleanly cleaned up. | |
468 """ | |
469 self.unsetEnvironment() | |
470 # Normally we wouldn't "clean up" the filesystem like this (leaving | |
471 # things for post-test inspection), but if we left the permissions the | |
472 # way they were, we'd be leaving files around that the buildbots | |
473 # couldn't delete, and that would be bad. | |
474 self.unlockSystem() | |
475 | |
476 | |
477 def test_developmentPluginAvailability(self): | |
478 """ | |
479 Plugins added in the development path should be loadable, even when | |
480 the (now non-importable) system path contains its own idea of the | |
481 list of plugins for a package. Inversely, plugins added in the | |
482 system path should not be available. | |
483 """ | |
484 # Run 3 times: uncached, cached, and then cached again to make sure we | |
485 # didn't overwrite / corrupt the cache on the cached try. | |
486 for x in range(3): | |
487 names = self.getAllPlugins() | |
488 names.sort() | |
489 self.assertEqual(names, ['app', 'dev']) | |
490 | |
491 | |
492 def test_freshPyReplacesStalePyc(self): | |
493 """ | |
494 Verify that if a stale .pyc file on the PYTHONPATH is replaced by a | |
495 fresh .py file, the plugins in the new .py are picked up rather than | |
496 the stale .pyc, even if the .pyc is still around. | |
497 """ | |
498 mypath = self.appPackage.child("stale.py") | |
499 mypath.setContent(pluginFileContents('one')) | |
500 # Make it super stale | |
501 x = time.time() - 1000 | |
502 os.utime(mypath.path, (x, x)) | |
503 pyc = mypath.sibling('stale.pyc') | |
504 # compile it | |
505 compileall.compile_dir(self.appPackage.path, quiet=1) | |
506 os.utime(pyc.path, (x, x)) | |
507 # Eliminate the other option. | |
508 mypath.remove() | |
509 # Make sure it's the .pyc path getting cached. | |
510 self.resetEnvironment() | |
511 # Sanity check. | |
512 self.assertIn('one', self.getAllPlugins()) | |
513 self.failIfIn('two', self.getAllPlugins()) | |
514 self.resetEnvironment() | |
515 mypath.setContent(pluginFileContents('two')) | |
516 self.failIfIn('one', self.getAllPlugins()) | |
517 self.assertIn('two', self.getAllPlugins()) | |
518 | |
519 | |
520 def test_newPluginsOnReadOnlyPath(self): | |
521 """ | |
522 Verify that a failure to write the dropin.cache file on a read-only | |
523 path will not affect the list of plugins returned. | |
524 | |
525 Note: this test should pass on both Linux and Windows, but may not | |
526 provide useful coverage on Windows due to the different meaning of | |
527 "read-only directory". | |
528 """ | |
529 self.unlockSystem() | |
530 self.sysplug.child('newstuff.py').setContent(pluginFileContents('one')) | |
531 self.lockSystem() | |
532 | |
533 # Take the developer path out, so that the system plugins are actually | |
534 # examined. | |
535 sys.path.remove(self.devPath.path) | |
536 | |
537 # Sanity check to make sure we're only flushing the error logged | |
538 # below... | |
539 self.assertEqual(len(self.flushLoggedErrors()), 0) | |
540 self.assertIn('one', self.getAllPlugins()) | |
541 self.assertEqual(len(self.flushLoggedErrors()), 1) | |
542 | |
543 | |
544 | |
545 class AdjacentPackageTests(unittest.TestCase): | |
546 """ | |
547 Tests for the behavior of the plugin system when there are multiple | |
548 installed copies of the package containing the plugins being loaded. | |
549 """ | |
550 | |
551 def setUp(self): | |
552 """ | |
553 Save the elements of C{sys.path} and the items of C{sys.modules}. | |
554 """ | |
555 self.originalPath = sys.path[:] | |
556 self.savedModules = sys.modules.copy() | |
557 | |
558 | |
559 def tearDown(self): | |
560 """ | |
561 Restore C{sys.path} and C{sys.modules} to their original values. | |
562 """ | |
563 sys.path[:] = self.originalPath | |
564 sys.modules.clear() | |
565 sys.modules.update(self.savedModules) | |
566 | |
567 | |
568 def createDummyPackage(self, root, name, pluginName): | |
569 """ | |
570 Create a directory containing a Python package named I{dummy} with a | |
571 I{plugins} subpackage. | |
572 | |
573 @type root: L{FilePath} | |
574 @param root: The directory in which to create the hierarchy. | |
575 | |
576 @type name: C{str} | |
577 @param name: The name of the directory to create which will contain | |
578 the package. | |
579 | |
580 @type pluginName: C{str} | |
581 @param pluginName: The name of a module to create in the | |
582 I{dummy.plugins} package. | |
583 | |
584 @rtype: L{FilePath} | |
585 @return: The directory which was created to contain the I{dummy} | |
586 package. | |
587 """ | |
588 directory = root.child(name) | |
589 package = directory.child('dummy') | |
590 package.makedirs() | |
591 package.child('__init__.py').setContent('') | |
592 plugins = package.child('plugins') | |
593 plugins.makedirs() | |
594 plugins.child('__init__.py').setContent(pluginInitFile) | |
595 pluginModule = plugins.child(pluginName + '.py') | |
596 pluginModule.setContent(pluginFileContents(name)) | |
597 return directory | |
598 | |
599 | |
600 def test_hiddenPackageSamePluginModuleNameObscured(self): | |
601 """ | |
602 Only plugins from the first package in sys.path should be returned by | |
603 getPlugins in the case where there are two Python packages by the same | |
604 name installed, each with a plugin module by a single name. | |
605 """ | |
606 root = FilePath(self.mktemp()) | |
607 root.makedirs() | |
608 | |
609 firstDirectory = self.createDummyPackage(root, 'first', 'someplugin') | |
610 secondDirectory = self.createDummyPackage(root, 'second', 'someplugin') | |
611 | |
612 sys.path.append(firstDirectory.path) | |
613 sys.path.append(secondDirectory.path) | |
614 | |
615 import dummy.plugins | |
616 | |
617 plugins = list(plugin.getPlugins(ITestPlugin, dummy.plugins)) | |
618 self.assertEqual(['first'], [p.__name__ for p in plugins]) | |
619 | |
620 | |
621 def test_hiddenPackageDifferentPluginModuleNameObscured(self): | |
622 """ | |
623 Plugins from the first package in sys.path should be returned by | |
624 getPlugins in the case where there are two Python packages by the same | |
625 name installed, each with a plugin module by a different name. | |
626 """ | |
627 root = FilePath(self.mktemp()) | |
628 root.makedirs() | |
629 | |
630 firstDirectory = self.createDummyPackage(root, 'first', 'thisplugin') | |
631 secondDirectory = self.createDummyPackage(root, 'second', 'thatplugin') | |
632 | |
633 sys.path.append(firstDirectory.path) | |
634 sys.path.append(secondDirectory.path) | |
635 | |
636 import dummy.plugins | |
637 | |
638 plugins = list(plugin.getPlugins(ITestPlugin, dummy.plugins)) | |
639 self.assertEqual(['first'], [p.__name__ for p in plugins]) | |
640 | |
641 | |
642 | |
643 class PackagePathTests(unittest.TestCase): | |
644 """ | |
645 Tests for L{plugin.pluginPackagePaths} which constructs search paths for | |
646 plugin packages. | |
647 """ | |
648 | |
649 def setUp(self): | |
650 """ | |
651 Save the elements of C{sys.path}. | |
652 """ | |
653 self.originalPath = sys.path[:] | |
654 | |
655 | |
656 def tearDown(self): | |
657 """ | |
658 Restore C{sys.path} to its original value. | |
659 """ | |
660 sys.path[:] = self.originalPath | |
661 | |
662 | |
663 def test_pluginDirectories(self): | |
664 """ | |
665 L{plugin.pluginPackagePaths} should return a list containing each | |
666 directory in C{sys.path} with a suffix based on the supplied package | |
667 name. | |
668 """ | |
669 foo = FilePath('foo') | |
670 bar = FilePath('bar') | |
671 sys.path = [foo.path, bar.path] | |
672 self.assertEqual( | |
673 plugin.pluginPackagePaths('dummy.plugins'), | |
674 [foo.child('dummy').child('plugins').path, | |
675 bar.child('dummy').child('plugins').path]) | |
676 | |
677 | |
678 def test_pluginPackagesExcluded(self): | |
679 """ | |
680 L{plugin.pluginPackagePaths} should exclude directories which are | |
681 Python packages. The only allowed plugin package (the only one | |
682 associated with a I{dummy} package which Python will allow to be | |
683 imported) will already be known to the caller of | |
684 L{plugin.pluginPackagePaths} and will most commonly already be in | |
685 the C{__path__} they are about to mutate. | |
686 """ | |
687 root = FilePath(self.mktemp()) | |
688 foo = root.child('foo').child('dummy').child('plugins') | |
689 foo.makedirs() | |
690 foo.child('__init__.py').setContent('') | |
691 sys.path = [root.child('foo').path, root.child('bar').path] | |
692 self.assertEqual( | |
693 plugin.pluginPackagePaths('dummy.plugins'), | |
694 [root.child('bar').child('dummy').child('plugins').path]) | |
OLD | NEW |