| 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 |