| 1 | """The isolation plugin resets the contents of sys.modules after running |
|---|
| 2 | each test module or package. Use it by setting ``--with-isolation`` or the |
|---|
| 3 | NOSE_WITH_ISOLATION environment variable. |
|---|
| 4 | |
|---|
| 5 | The effects are similar to wrapping the following functions around the |
|---|
| 6 | import and execution of each test module:: |
|---|
| 7 | |
|---|
| 8 | def setup(module): |
|---|
| 9 | module._mods = sys.modules.copy() |
|---|
| 10 | |
|---|
| 11 | def teardown(module): |
|---|
| 12 | to_del = [ m for m in sys.modules.keys() if m not in |
|---|
| 13 | module._mods ] |
|---|
| 14 | for mod in to_del: |
|---|
| 15 | del sys.modules[mod] |
|---|
| 16 | sys.modules.update(module._mods) |
|---|
| 17 | |
|---|
| 18 | Isolation works only during lazy loading. In normal use, this is only |
|---|
| 19 | during discovery of modules within a directory, where the process of |
|---|
| 20 | importing, loading tests and running tests from each module is |
|---|
| 21 | encapsulated in a single loadTestsFromName call. This plugin |
|---|
| 22 | implements loadTestsFromNames to force the same lazy-loading there, |
|---|
| 23 | which allows isolation to work in directed mode as well as discovery, |
|---|
| 24 | at the cost of some efficiency: lazy-loading names forces full context |
|---|
| 25 | setup and teardown to run for each name, defeating the grouping that |
|---|
| 26 | is normally used to ensure that context setup and teardown are run the |
|---|
| 27 | fewest possible times for a given set of names. |
|---|
| 28 | |
|---|
| 29 | .. warning :: |
|---|
| 30 | |
|---|
| 31 | This plugin should not be used in conjunction with other plugins |
|---|
| 32 | that assume that modules, once imported, will stay imported; for |
|---|
| 33 | instance, it may cause very odd results when used with the coverage |
|---|
| 34 | plugin. |
|---|
| 35 | |
|---|
| 36 | """ |
|---|
| 37 | |
|---|
| 38 | import logging |
|---|
| 39 | import sys |
|---|
| 40 | |
|---|
| 41 | from nose.plugins import Plugin |
|---|
| 42 | |
|---|
| 43 | |
|---|
| 44 | log = logging.getLogger('nose.plugins.isolation') |
|---|
| 45 | |
|---|
| 46 | class IsolationPlugin(Plugin): |
|---|
| 47 | """ |
|---|
| 48 | Activate the isolation plugin to isolate changes to external |
|---|
| 49 | modules to a single test module or package. The isolation plugin |
|---|
| 50 | resets the contents of sys.modules after each test module or |
|---|
| 51 | package runs to its state before the test. PLEASE NOTE that this |
|---|
| 52 | plugin should not be used with the coverage plugin, or in any other case |
|---|
| 53 | where module reloading may produce undesirable side-effects. |
|---|
| 54 | """ |
|---|
| 55 | score = 10 # I want to be last |
|---|
| 56 | name = 'isolation' |
|---|
| 57 | |
|---|
| 58 | def configure(self, options, conf): |
|---|
| 59 | """Configure plugin. |
|---|
| 60 | """ |
|---|
| 61 | Plugin.configure(self, options, conf) |
|---|
| 62 | self._mod_stack = [] |
|---|
| 63 | |
|---|
| 64 | def beforeContext(self): |
|---|
| 65 | """Copy sys.modules onto my mod stack |
|---|
| 66 | """ |
|---|
| 67 | mods = sys.modules.copy() |
|---|
| 68 | self._mod_stack.append(mods) |
|---|
| 69 | |
|---|
| 70 | def afterContext(self): |
|---|
| 71 | """Pop my mod stack and restore sys.modules to the state |
|---|
| 72 | it was in when mod stack was pushed. |
|---|
| 73 | """ |
|---|
| 74 | mods = self._mod_stack.pop() |
|---|
| 75 | to_del = [ m for m in sys.modules.keys() if m not in mods ] |
|---|
| 76 | if to_del: |
|---|
| 77 | log.debug('removing sys modules entries: %s', to_del) |
|---|
| 78 | for mod in to_del: |
|---|
| 79 | del sys.modules[mod] |
|---|
| 80 | sys.modules.update(mods) |
|---|
| 81 | |
|---|
| 82 | def loadTestsFromNames(self, names, module=None): |
|---|
| 83 | """Create a lazy suite that calls beforeContext and afterContext |
|---|
| 84 | around each name. The side-effect of this is that full context |
|---|
| 85 | fixtures will be set up and torn down around each test named. |
|---|
| 86 | """ |
|---|
| 87 | # Fast path for when we don't care |
|---|
| 88 | if not names or len(names) == 1: |
|---|
| 89 | return |
|---|
| 90 | loader = self.loader |
|---|
| 91 | plugins = self.conf.plugins |
|---|
| 92 | def lazy(): |
|---|
| 93 | for name in names: |
|---|
| 94 | plugins.beforeContext() |
|---|
| 95 | yield loader.loadTestsFromName(name, module=module) |
|---|
| 96 | plugins.afterContext() |
|---|
| 97 | return (loader.suiteClass(lazy), []) |
|---|
| 98 | |
|---|
| 99 | def prepareTestLoader(self, loader): |
|---|
| 100 | """Get handle on test loader so we can use it in loadTestsFromNames. |
|---|
| 101 | """ |
|---|
| 102 | self.loader = loader |
|---|
| 103 | |
|---|