Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(69)

Side by Side Diff: third_party/twisted_8_1/twisted/test/test_plugin.py

Issue 12261012: Remove third_party/twisted_8_1 (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/build
Patch Set: Created 7 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(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])
OLDNEW
« no previous file with comments | « third_party/twisted_8_1/twisted/test/test_persisted.py ('k') | third_party/twisted_8_1/twisted/test/test_policies.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698