~cbehrens/nova/lp844160-build-works-with-zones

« back to all changes in this revision

Viewing changes to vendor/Twisted-10.0.0/twisted/test/test_plugin.py

  • Committer: Jesse Andrews
  • Date: 2010-05-28 06:05:26 UTC
  • Revision ID: git-v1:bf6e6e718cdc7488e2da87b21e258ccc065fe499
initial commit

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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])